Skip to content

Repository files navigation

RootResolver

RootResolver is a minimal, public, unauthenticated HTTP service that resolves a RecordWeb namespace or a full did:rwp to the resolverEndpoint responsible for it, by querying the global-namespace-registry.

It has no UI and no client-facing authentication. Any application that only needs "which resolver is responsible for this namespace" can call it directly, without ever holding a Fabric identity of its own.

Why this exists

Querying a Fabric peer requires a signed, TLS-authenticated identity. That is not something a public-facing application should ever ask an anonymous caller for, and it is not something most callers should need to care about at all. RootResolver holds exactly one, operator-configured Fabric identity and exposes a single read-only lookup on top of it. This mirrors how a public DNS resolver works: callers send a plain query and get back an answer, with all cryptographic and network complexity contained on the server side.

API

GET /resolve?id=<value>

id is either:

  • a raw Global Namespace Identifier (canonical UUIDv4, per RWP-23), or
  • a full did:rwp:<namespace>:... identifier, from which the namespace is extracted automatically.

Success — 200 OK

{
  "namespace": "a3f9e21c-...",
  "resolverEndpoint": "https://records.recordweb.org/api/v1/records",
  "cacheAge": 0
}

cacheAge is the number of seconds since this namespace was last resolved against the chaincode; 0 means the answer came fresh from the network just now.

Errors

Status Meaning
400 id is missing or is neither a valid namespace nor a did:rwp:... identifier.
404 The namespace is syntactically valid but not registered in the namespace registry.
502 The Fabric network or chaincode could not be reached or returned an error.

By design, the response contains only namespace, resolverEndpoint, and cacheAge — no registration metadata (registeredBy, registeredAt, txId, endorsements, etc.). RootResolver answers "where do I ask about this namespace", nothing more; anyone needing full registry metadata should use an admin-facing tool such as rw-gnr-admin instead.

GET /health

Returns 200 OK with { "status": "ok", "cache": { "ttlSeconds": ..., "entries": ... } }. Does not touch the Fabric network — safe to use as a lightweight container/orchestrator liveness probe.

Caching

Resolved namespaces are kept in a per-process, in-memory Map with a configurable TTL (CACHE_TTL_SECONDS, default 60). There is no external cache store (e.g. Redis): a namespace-registry lookup is cheap enough that per-instance caching is sufficient to absorb repeated queries for the same namespace, and it keeps the service genuinely minimal and stateless-per-restart. Set CACHE_TTL_SECONDS=0 to disable caching entirely and always hit the chaincode.

If you run multiple replicas of RootResolver behind a load balancer, each replica maintains its own cache independently — this is an intentional trade-off for simplicity over perfectly synchronized caching, appropriate for routing data that changes rarely.

Architecture

Caller
   │  GET /resolve?id=did:rwp:<namespace>:... (or ?id=<namespace>)
   ▼
server.js ──▶ cache.js (namespace → resolverEndpoint, TTL)
   │               │ miss
   │               ▼
   └────────▶ fabricConnect.js ──▶ Fabric peer (gRPC/mTLS) ──▶ namespace-registry chaincode
  • server.js – Express app; the only HTTP surface. Validates id, checks the cache, falls through to the chaincode on a miss, and strips the response down to namespace / resolverEndpoint / cacheAge.
  • fabricConnect.js – Fabric Gateway client. Unlike RecordFinder, the gRPC client, identity, and signer are built once and reused across requests (no per-request peer selection here), keeping the hot path to a single evaluateTransaction call.
  • cache.js – The in-memory TTL cache described above.

There is deliberately no frontend/ directory and no static assets — this service is meant to be called by other services and applications, not opened in a browser.

Prerequisites

  • Access to a peer of the RootResolver network and a Fabric identity (certificate + private key) with read access to the relevant channel.
  • Node.js 20+ if running without Docker.

Configuration

All values are set via environment variables, see .env.example:

Variable Meaning Default
PORT HTTP port of the service 3000
CRYPTO_CONFIG_HOST_PATH Path to the local crypto material (outside the container) /opt/rw-rrn/crypto-config
CRYPTO_DIR Base directory of the MSP/TLS crypto material inside the container /crypto/peerOrganizations/tws.rwrrn.recordweb.dev
MSP_ID MSP ID of the service's Fabric identity TWSOrgMSP
ORG_DOMAIN Organisation domain, used to derive the default admin identity name org.recordweb.dev
ADMIN_IDENTITY Name of the admin identity as registered/enrolled with the Fabric CA (live-CA networks typically use a custom name, e.g. tws-org-admin, rather than the cryptogen-style Admin@<org-domain>) Admin@${ORG_DOMAIN}
ADMIN_CERT_FILENAME Filename of the signing certificate under users/<ADMIN_IDENTITY>/msp/signcerts/ (fabric-ca-client enroll typically produces a plain cert.pem) ${ADMIN_IDENTITY}-cert.pem
CHANNEL_NAME Fabric channel of the RootResolver network rw-gnr-test
CHAINCODE_NAME Name of the namespace-registry chaincode namespace-registry
PEER_ENDPOINT The single peer this instance talks to (host:port) peer0.tws.rwrrn.recordweb.dev:7051
PEER_HOST_ALIAS TLS SAN hostname of that peer peer0.tws.rwrrn.recordweb.dev
CACHE_TTL_SECONDS In-memory cache TTL per namespace, in seconds (0 disables caching) 60

Running locally

npm install
cp .env.example .env   # adjust values
node server.js
curl "http://localhost:3000/resolve?id=did:rwp:a3f9e21c-....:xyz123"
curl "http://localhost:3000/health"

Running with Docker

cp .env.example .env   # adjust values, including CRYPTO_CONFIG_HOST_PATH
docker compose up -d --build

docker-compose.yml mounts the crypto material read-only into the container at /crypto, sourced from the host path configured via CRYPTO_CONFIG_HOST_PATH in .env (this variable must live in the same .env file as docker-compose.yml, since Compose only interpolates ${...} placeholders from a root-level .env, not from variables passed into the container via env_file). It also joins the external tws-proxy Docker network so a shared nginx reverse proxy can reach the container by its Docker DNS name (rootresolver).

Deployment to a VPS is automated via .github/workflows/deploy.yml (SSH deploy on push to main), provided the secrets VPS_HOST, VPS_USER, and VPS_SSH_KEY are configured in the repository and /opt/rootresolver exists on the VPS with a checked-out copy of this repository.

Security notes

  • The service performs read-only operations exclusively (evaluateTransaction, never submitTransaction); it cannot modify namespace registrations.
  • It exposes no Fabric identity, MSP, channel, chaincode, or peer selection to its callers — the entire API surface is the two GET endpoints described above.
  • Crypto material (private keys, admin certificates) never leaves the container filesystem and should be protected on the host with restrictive file permissions or a secrets manager.
  • Because /resolve is unauthenticated by design, consider fronting it with basic rate limiting at the reverse-proxy level if it is exposed to the public internet at scale.

Relation to RecordWeb

RootResolver implements the read side of the namespace-resolution model specified in RWP: given a namespace, find the resolver responsible for it, without ever exposing DID documents, records, or registry metadata beyond the routing answer itself. Details on network setup, governance, and the rollout plan for the RootResolver network are available in the RootResolver Network Operating Handbook maintained by the RecordWeb organisation.

License

MIT

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages