Skip to content

feat(gateway): auto Hydra registration + OAuth token acquisition (Phase 1b)#473

Merged
raahulrahl merged 4 commits intomainfrom
feat/gateway-did-hydra-autoreg
Apr 19, 2026
Merged

feat(gateway): auto Hydra registration + OAuth token acquisition (Phase 1b)#473
raahulrahl merged 4 commits intomainfrom
feat/gateway-did-hydra-autoreg

Conversation

@raahulrahl
Copy link
Copy Markdown
Contributor

@raahulrahl raahulrahl commented Apr 19, 2026

Closes the manual-Hydra-registration step from Phase 1a. Set five env vars, start the gateway, everything else is automatic.

What this adds

Piece File What it does
Hydra admin client gateway/src/bindu/identity/hydra-admin.ts Idempotent GET-then-POST at /admin/clients. client_secret derived from seed (no disk state).
Token provider gateway/src/bindu/identity/hydra-token.ts In-memory token cache + proactive refresh + concurrent-caller coalescing
did_signed fallback gateway/src/bindu/auth/resolver.ts tokenEnvVar now optional — omitted → falls back to gateway's token provider
Bootstrap wiring gateway/src/index.ts setupHydraIntegration(identity) on boot: reads env, registers, creates provider
Docs .env.example, README.md Auto vs Manual mode, peer-config precedence, failure-mode table

End-to-end operator flow (auto mode)

export BINDU_GATEWAY_DID_SEED="$(python -c 'import os,base64;print(base64.b64encode(os.urandom(32)).decode())')"
export BINDU_GATEWAY_AUTHOR=ops@example.com
export BINDU_GATEWAY_NAME=gateway
export BINDU_GATEWAY_HYDRA_ADMIN_URL=http://hydra:4445
export BINDU_GATEWAY_HYDRA_TOKEN_URL=http://hydra:4444/oauth2/token

npm run dev
# → DID identity loaded: did:bindu:ops_at_example_com:gateway:...
# → public key (base58): 4zvwRjXUKGfvwn...
# → registering with Hydra at http://hydra:4445...
# → Hydra registration confirmed for did:bindu:...

Peer config now becomes:

{ "url": "http://agent:3773", "auth": { "type": "did_signed" } }

No manual hydra create oauth2-client, no per-peer tokens stashed in env vars, no file on disk beyond the seed that was already required.

Four commits, reviewable in order

  1. cd7fab7 — Hydra admin client. 334 lines. ensureHydraClient idempotent registration + deriveClientSecret derivation-from-seed. 9 tests covering GET 200 (no POST), GET 404 → POST (correct payload shape), GET 5xx / POST 4xx surfacing body, grant types override, trailing slash tolerance.

  2. 43a5a07 — OAuth token provider. 310 lines. createTokenProvider with in-memory cache + proactive refresh + concurrent-caller coalescing. 7 tests covering cache hit, form body shape, controlled-clock refresh, stampede coalescing, Hydra 4xx surfacing, malformed response rejection, post-error recovery.

  3. e95939fdid_signed fallback. 161 lines. tokenEnvVar optional; resolution order = peer env var > gateway provider > error. 3 new tests covering provider fallback, peer-scoped precedence, no-option error.

  4. 40b4953 — Bootstrap + docs. 241 lines. setupHydraIntegration(identity) in main(), partial-Hydra-config fail-fast, .env.example + README rewrite with Auto/Manual split.

Failure mode table (also in README)

Scenario When Error
Seed malformed Boot BINDU_GATEWAY_DID_SEED must decode to exactly 32 bytes
Partial identity config Boot Partial DID identity config — set all three or none
Partial Hydra config Boot Partial Hydra config — set both or neither
Hydra admin unreachable Boot Hydra admin GET /admin/clients/... returned 503: ...
did_signed peer, no identity First call did_signed peer requires a gateway LocalIdentity
did_signed peer, no token source First call clear error naming both tokenEnvVar and BINDU_GATEWAY_HYDRA_TOKEN_URL

Fail-fast-at-boot where possible; clear-error-at-call-time where config isn't testable until a peer is actually called.

Test summary

99 gateway tests pass (77 baseline from Phase 1a + 22 new across four new files/sections).

Deferred to later phases

  • Phase 1c — frontend POC (localStorage + tweetnacl, per Decision 4)
  • Phase 1d — Postman collection with the corrected signing pre-request script
  • Phase 1e — deep docs in docs/GATEWAY_DID_SETUP.md
  • Future/.well-known/did.json endpoint on the gateway (A2A-native DID resolution — peers could skip Hydra introspection in favor of resolving the gateway's DID directly). Not needed for MVP since Hydra metadata carries the public key.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Hydra auto-registration for OAuth clients with automatic token refresh and caching.
    • Added optional "Auto" setup mode for DID signing that eliminates per-peer token configuration requirements.
  • Documentation

    • Expanded setup guide documenting Auto and Manual federation modes for DID signing.
    • Added failure modes table with clear error messages for configuration issues.
  • Tests

    • Added comprehensive test coverage for OAuth token management and client registration workflows.

raahulrahl and others added 4 commits April 19, 2026 17:51
Phase 1b Commit 1. Adds the building block that lets the gateway
self-register with Hydra on boot, removing the manual
"hydra create oauth2-client" step from the operator's enable path.

New file: gateway/src/bindu/identity/hydra-admin.ts

  * ensureHydraClient({adminUrl, did, publicKeyBase58, clientSecret,
    scope, ...}) — idempotent. GETs /admin/clients/{did} first; if
    200 the client already exists and the function is a no-op. If
    404, POSTs /admin/clients with the full payload. Any other
    status (5xx, auth failure) surfaces the Hydra response body so
    operators can diagnose.

  * deriveClientSecret(seed) — derives the OAuth client_secret
    from the gateway's Ed25519 seed using sha256(seed ‖ scoped-tag)
    → base64url. Avoids needing any persistent state beyond the
    seed env var. Anyone with the seed can compute the secret, but
    anyone with the seed can already impersonate the gateway by
    signing as it, so the derivation adds no new trust boundary.

  * buildClientPayload — matches bindu/auth/hydra/registration.py's
    shape byte-for-byte so the same Hydra can host both gateways
    and agents without schema drift: client_id=DID,
    client_name=human-readable, grant_types=["client_credentials"]
    by default, token_endpoint_auth_method="client_secret_post",
    and metadata carrying public_key, key_type, verification_method,
    hybrid_auth=true, registered_at timestamp.

Tests: gateway/tests/bindu/hydra-admin.test.ts — 9 cases

  * deriveClientSecret: deterministic for same seed ; different
    seeds produce different secrets ; output is base64url (no
    +/= padding).

  * ensureHydraClient: GET 200 → no POST ; GET 404 → POST with
    expected payload shape (all fields asserted including metadata
    contents) ; GET 5xx surfaces body ; POST 4xx after 404 GET
    surfaces body ; grantTypes override works ; trailing slash on
    adminUrl is tolerated (no double-slash in constructed URL).

All 86 gateway tests pass (77 previous + 9 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1b Commit 2. Adds the token acquisition / caching module the
gateway uses to get a Hydra-issued OAuth bearer without an
operator-supplied env var. Pair with hydra-admin (Commit 1) to
fully automate the gateway's side of the DID auth handshake.

New file: gateway/src/bindu/identity/hydra-token.ts

  * createTokenProvider({tokenUrl, clientId, clientSecret, scope,
    refreshThresholdSec, ...}) — returns a TokenProvider with a
    single method getToken(): Promise<string>.

  * Cache + proactive refresh: tokens are cached in-memory with an
    absolute expiry timestamp. getToken() returns the cached token
    until it has less than refreshThresholdSec (default 30s) of
    runway, then fetches a fresh one.

  * Concurrent-caller coalescing: N parallel getToken() calls
    during a refresh share a single in-flight promise. Stops a
    plan's tool-call fan-out from each triggering its own Hydra
    hit — matters for latency, rate limits, and clean telemetry.

  * Hydra contract: POSTs form-encoded
    grant_type=client_credentials + client_id + client_secret +
    scope to the token endpoint (per RFC 6749 §4.4). Parses the
    {access_token, expires_in} response. Non-2xx surfaces the body
    so operators can diagnose 401s (invalid_client) vs 5xx
    (Hydra down) vs 429 (rate limit).

  * In-memory only by design: disk/Redis persistence would add
    failure modes without improving the common path — we fetch at
    cold-start anyway because ensureHydraClient runs right before.

Tests: gateway/tests/bindu/hydra-token.test.ts — 7 cases

  * First call fetches; 2nd and 3rd calls return cached string
    without re-fetching.
  * Form body shape sanity-check: grant_type, client_id,
    client_secret, scope are all form-encoded correctly.
  * Proactive refresh: controlled clock advances past the
    threshold, next getToken() fetches fresh. Asserts the exact
    fetch count.
  * Coalescing: three concurrent getToken() calls before the fetch
    resolves share a single in-flight promise, all resolve to the
    same token, fetchMock called once.
  * Hydra 4xx surfaces status + body.
  * Malformed response (missing access_token or expires_in) is
    rejected with a clear error — no silent cache of garbage.
  * Recovery: a failed fetch doesn't leave the provider stuck;
    the next call retries.

All 93 gateway tests pass (86 previous + 7 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1b Commit 3. Completes the auto-path plumbing so a peer
configured with bare { type: "did_signed" } (no tokenEnvVar) pulls
its OAuth bearer from the gateway's auto-acquired token provider
instead of requiring an operator-supplied env var per peer.

Auth resolver (gateway/src/bindu/auth/resolver.ts):

  * PeerAuth.did_signed.tokenEnvVar is now optional. Three resolution
    modes, tried in order:
      1. tokenEnvVar set on the peer → read token from that env var
         (unchanged — the federated-deploy escape hatch)
      2. Gateway has a TokenProvider configured → getToken()
         (the common single-Hydra auto path from Phase 1b)
      3. Neither → throw a clear error naming both options

  * buildAuthHeaders gains an optional fourth tokenProvider
    parameter. Identity is still required (the signer needs it);
    the token source is now flexible.

RPC / client plumbing:

  * gateway/src/bindu/client/fetch.ts — RpcInput grows a
    tokenProvider field; rpc() threads it into buildAuthHeaders.
  * gateway/src/bindu/client/poll.ts — SendAndPollInput + baseRpc
    grow the field.
  * gateway/src/bindu/client/index.ts — makeLayer(identity?,
    tokenProvider?) takes both; runCall and runCancel accept both
    and pass them through.

Existing test coverage:

  * The previous did_signed test (tokenEnvVar set) still passes
    unchanged.
  * The schema test updated: `{ type: "did_signed" }` alone is now
    valid (it used to require tokenEnvVar); `tokenEnvVar: 42` is
    still rejected on type grounds.

New tests (3 cases in auth-resolver.test.ts):

  * Fallback: did_signed without tokenEnvVar + a provider → pulls
    token from provider, produces correct signed headers, provider
    called exactly once.
  * Precedence: when both tokenEnvVar and provider are set, the
    peer-scoped env var wins (explicit-over-implicit; matches the
    federated-deploy story where each peer has its own Hydra).
  * Failure: neither tokenEnvVar nor provider → clear error
    pointing at both BINDU_GATEWAY_HYDRA_TOKEN_URL (the auto path)
    and the peer-scoped escape hatch.

All 96 gateway tests pass (93 previous + 3 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1b Commit 4 (final). Wires ensureHydraClient and
createTokenProvider into main() so operators get end-to-end auto:
set the identity + Hydra URLs, start the gateway, everything else
is handled.

gateway/src/index.ts

  * buildAppLayer now takes an optional TokenProvider alongside
    identity, passes both into BinduClient.makeLayer.

  * setupHydraIntegration(identity) is the new boot-time async
    hook. Reads three env vars:
        BINDU_GATEWAY_HYDRA_ADMIN_URL  (e.g. http://hydra:4445)
        BINDU_GATEWAY_HYDRA_TOKEN_URL  (e.g. http://hydra:4444/oauth2/token)
        BINDU_GATEWAY_HYDRA_SCOPE      (space-separated; default:
                                        "openid offline agent:read agent:write")
    Both URLs unset → returns undefined (manual/federated path still
    works, just no auto token). One URL set without the other →
    throws with a clear "Partial Hydra config" error. Both set →
    calls ensureHydraClient (idempotent) then returns a
    TokenProvider.

  * Registration blocks boot completion — we want to fail fast if
    the admin URL is unreachable rather than discover the problem
    on the first peer call. The log line ordering:
        DID identity loaded: ...
        public key (base58): ...
        registering with Hydra at ...
        Hydra registration confirmed for ...

  * Deterministic client_secret: re-derived from the seed at boot
    (not plumbed through from loadLocalIdentity, which doesn't
    expose raw bytes by design). Same derivation as Commit 1
    documented in hydra-admin.ts.

Tests: gateway/tests/index-identity.test.ts — 3 new cases

  * No Hydra env vars → returns undefined (manual/federated mode).
  * Admin URL set without token URL → throws with "Partial Hydra
    config" (pointing at both vars).
  * Token URL set without admin URL → same error (symmetric).

The happy path (both set, registration succeeds, TokenProvider
returned) is covered end-to-end by the component tests in Commits
1 and 2 — ensureHydraClient's idempotent GET-then-POST and
createTokenProvider's cache + concurrency guarantees.

Docs:

  * gateway/.env.example — three new Hydra vars documented with
    the same "set both or neither" fail-fast rule.

  * gateway/README.md — rewrote the "DID signing for downstream
    peers" section with an Auto/Manual split. Shows both setup
    flows side by side, explains peer-config precedence
    (peer-scoped tokenEnvVar wins over gateway provider), lists
    every failure mode with the exact error text.

All 99 gateway tests pass.

Phase 1b end-to-end: set four env vars (DID_SEED, AUTHOR, NAME,
HYDRA_ADMIN_URL, HYDRA_TOKEN_URL), restart, peers configured with
bare { type: "did_signed" } just work. No manual Hydra commands,
no per-peer env vars, no disk state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d3d965e3-ba6a-43ba-bf84-26e0ab65f405

📥 Commits

Reviewing files that changed from the base of the PR and between 399e673 and 40b4953.

📒 Files selected for processing (13)
  • gateway/.env.example
  • gateway/README.md
  • gateway/src/bindu/auth/resolver.ts
  • gateway/src/bindu/client/fetch.ts
  • gateway/src/bindu/client/index.ts
  • gateway/src/bindu/client/poll.ts
  • gateway/src/bindu/identity/hydra-admin.ts
  • gateway/src/bindu/identity/hydra-token.ts
  • gateway/src/index.ts
  • gateway/tests/bindu/auth-resolver.test.ts
  • gateway/tests/bindu/hydra-admin.test.ts
  • gateway/tests/bindu/hydra-token.test.ts
  • gateway/tests/index-identity.test.ts

📝 Walkthrough

Walkthrough

This PR extends the Bindu gateway with Hydra OAuth2 integration, enabling automatic DID-signed peer authentication without per-peer token configuration. It introduces Hydra client registration at boot time, gateway-level token provisioning with caching and refresh, and optional token provider threading through the RPC client layer. Documentation, comprehensive tests, and updated configuration examples accompany the feature.

Changes

Cohort / File(s) Summary
Documentation
gateway/.env.example, gateway/README.md
Extended configuration examples and setup documentation with Hydra URLs, client registration flow, and two explicit DID-signing modes (Auto with gateway token provisioning, Manual federated with per-peer tokens). Structured failure modes and clarified behavior when identity/Hydra variables are omitted.
Hydra Integration
gateway/src/bindu/identity/hydra-admin.ts, gateway/src/bindu/identity/hydra-token.ts
New modules for Hydra OAuth client management: hydra-admin.ts performs idempotent client registration using gateway DID and derived client secret; hydra-token.ts implements TokenProvider for cached, proactively-refreshed OAuth2 client_credentials tokens with concurrent-request deduplication.
Auth & Token Resolution
gateway/src/bindu/auth/resolver.ts
Made tokenEnvVar optional in did_signed peer auth configuration; extended buildAuthHeaders with optional TokenProvider parameter and prioritized token resolution: env var (if set), provider (if no env var), or error referencing gateway Hydra setup.
Client Layer Wiring
gateway/src/bindu/client/fetch.ts, gateway/src/bindu/client/index.ts, gateway/src/bindu/client/poll.ts
Threaded optional TokenProvider parameter through RPC client factory (makeLayer), request sender (sendAndPoll), and poll operations (sendAndPoll in poll.ts) to enable automatic token acquisition for outbound did_signed calls.
Bootstrap & Integration
gateway/src/index.ts
Added setupHydraIntegration function to conditionally initialize Hydra client registration and token provisioning at startup; updated main() and buildAppLayer to wire token provider through the client layer when DID identity is configured.
Test Coverage
gateway/tests/bindu/auth-resolver.test.ts, gateway/tests/bindu/hydra-admin.test.ts, gateway/tests/bindu/hydra-token.test.ts, gateway/tests/index-identity.test.ts
Added/extended test suites covering optional tokenEnvVar handling, deterministic client secret derivation, idempotent Hydra client registration, OAuth token caching/refresh/concurrency, and boot-time Hydra integration validation.

Sequence Diagram(s)

sequenceDiagram
    participant Gateway as Gateway Boot
    participant HAdmin as Hydra Admin
    participant HTok as Hydra Token
    participant Peer as Peer (did_signed)
    
    rect rgba(100, 200, 150, 0.5)
    note over Gateway,HTok: Boot-time Hydra Setup
    Gateway->>HAdmin: GET /admin/clients/{gatewayDID}
    alt Client exists (200)
        HAdmin-->>Gateway: {clientId, clientSecret}
    else Client not found (404)
        Gateway->>HAdmin: POST /admin/clients with registration payload
        HAdmin-->>Gateway: {clientId, clientSecret}
    end
    Gateway->>HTok: POST /token with client_credentials grant
    HTok-->>Gateway: {access_token, expires_in}
    Gateway->>Gateway: Cache token with refresh threshold
    end
    
    rect rgba(150, 150, 200, 0.5)
    note over Gateway,Peer: Runtime RPC Call with Auto Token
    Peer->>Gateway: RPC call (did_signed peer, no tokenEnvVar)
    Gateway->>Gateway: Check cached token lifetime
    alt Token needs refresh
        Gateway->>HTok: POST /token with client_credentials grant
        HTok-->>Gateway: {access_token, expires_in}
        Gateway->>Gateway: Update cached token
    end
    Gateway->>Gateway: Sign request with DID private key
    Gateway->>Gateway: Add Authorization: Bearer {access_token}
    Gateway->>Peer: Signed RPC request with bearer token
    Peer-->>Gateway: Response
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related PRs

  • Bindu#463: Introduced the gateway foundation with DID identity support; this PR directly extends that work by adding Hydra auto-registration and token provisioning across the same client/auth/bootstrap layers.

Poem

🐰 Hopping through OAuth's misty garden,
A gateway finds its token trees,
No more begging each peer's pardon—
One DID signs with Hydra's breeze! ✨🔐

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gateway-did-hydra-autoreg

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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