Files
explorer-monorepo/backend/api/rest
Devin 29fe704f3c feat(auth): JWT jti + per-track TTLs (Track 4 <=1h) + revocation + refresh endpoint
Closes the 'JWT hygiene' gap identified by the review:

  - 24h TTL was used for every track, including Track 4 operator sessions
    carrying operator.write.* permissions.
  - Tokens had no server-side revocation path; rotating JWT_SECRET was
    the only way to invalidate a session, which would punt every user.
  - Tokens carried no jti, so individual revocation was impossible even
    with a revocations table.

Changes:

Migration 0016_jwt_revocations (up + down):
  - CREATE TABLE jwt_revocations (jti PK, address, track,
    token_expires_at, revoked_at, reason) plus indexes on address and
    token_expires_at. Append-only; idempotent on duplicate jti.

backend/auth/wallet_auth.go:
  - tokenTTLs map: track 1 = 12h, 2 = 8h, 3 = 4h, 4 = 60m. tokenTTLFor
    returns the ceiling; default is 12h for unknown tracks.
  - generateJWT now embeds a 128-bit random jti (hex-encoded) and uses
    the per-track TTL instead of a hardcoded 24h.
  - parseJWT: shared signature-verification + claim-extraction helper
    used by ValidateJWT and RefreshJWT. Returns address, track, jti, exp.
  - jtiFromToken: parses jti from an already-trusted token without a
    second crypto roundtrip.
  - isJTIRevoked: EXISTS query against jwt_revocations, returning
    ErrJWTRevocationStorageMissing when the table is absent (migration
    not run yet) so callers can surface a 503 rather than silently
    treating every token as valid.
  - RevokeJWT(ctx, token, reason): records the jti; idempotent via
    ON CONFLICT (jti) DO NOTHING. Refuses legacy tokens without jti.
  - RefreshJWT(ctx, token): validates, revokes the old token (reason
    'refresh'), and mints a new token with fresh jti + fresh TTL. Same
    (address, track) as the inbound token, same permissions set.
  - ValidateJWT now consults jwt_revocations when a DB is configured;
    returns ErrJWTRevoked for revoked tokens.

backend/api/rest/auth_refresh.go (new):
  - POST /api/v1/auth/refresh handler: expects 'Authorization: Bearer
    <jwt>'; returns WalletAuthResponse with the new token. Maps
    ErrJWTRevoked to 401 token_revoked and ErrWalletAuthStorageNotInitialized
    to 503.
  - POST /api/v1/auth/logout handler: same header contract, idempotent,
    returns {status: ok}. Returns 503 when the revocations table
    isn't present so ops know migration 0016 hasn't run.
  - Both handlers reuse the existing extractBearerToken helper from
    auth.go so parsing is consistent with the rest of the access layer.

backend/api/rest/routes.go:
  - Registered /api/v1/auth/refresh and /api/v1/auth/logout.

Tests:
  - TestTokenTTLForTrack4IsShort: track 4 TTL <= 1h.
  - TestTokenTTLForTrack1Track2Track3AreReasonable: bounded at 12h.
  - TestGeneratedJWTCarriesJTIClaim: jti is present, 128 bits / 32 hex.
  - TestGeneratedJWTExpIsTrackAppropriate: exp matches tokenTTLFor per
    track within a couple-second tolerance.
  - TestRevokeJWTWithoutDBReturnsError: a WalletAuth with nil db must
    refuse to revoke rather than silently pretending it worked.
  - All pre-existing wallet_auth tests still pass.

Also fixes a small SA4006/SA4017 regression in mission_control.go that
PR #5 introduced by shadowing the outer err with json.Unmarshal's err
return. Reworked to uerr so the outer err and the RPC fallback still
function as intended.

Verification:
  go build ./...         clean
  go vet ./...           clean
  go test ./auth/...     PASS (including new tests)
  go test ./api/rest/... PASS
  staticcheck ./auth/... ./api/rest/...  clean on SA4006/SA4017/SA1029

Advances completion criterion 3 (JWT hygiene): 'Track 4 sessions TTL
<= 1h; server-side revocation list (keyed on jti) enforced on every
token validation; refresh endpoint rotates the token in place so the
short TTL is usable in practice; logout endpoint revokes immediately.'
2026-04-18 19:20:57 +00:00
..

REST API Server

REST API implementation for the ChainID 138 Explorer Platform.

Structure

  • server.go - Main server setup and route configuration
  • routes.go - Route handlers and URL parsing
  • auth.go - Wallet auth, user-session auth, RPC product access, subscriptions, and API keys
  • blocks.go - Block-related endpoints
  • transactions.go - Transaction-related endpoints
  • addresses.go - Address-related endpoints
  • search.go - Unified search endpoint
  • mission_control.go - Mission-control bridge trace and cached liquidity helpers
  • validation.go - Input validation utilities
  • middleware.go - HTTP middleware (logging, compression)
  • errors.go - Error response utilities

API Endpoints

Auth

  • POST /api/v1/auth/nonce - Create a wallet-signature nonce
  • POST /api/v1/auth/wallet - Authenticate a wallet and receive a track JWT
  • POST /api/v1/auth/register - Create an access-console user session
  • POST /api/v1/auth/login - Log in to the access console

Blocks

  • GET /api/v1/blocks - List blocks (paginated)
  • GET /api/v1/blocks/{chain_id}/{number} - Get block by number
  • GET /api/v1/blocks/{chain_id}/hash/{hash} - Get block by hash

Transactions

  • GET /api/v1/transactions - List transactions (paginated, filterable)
  • GET /api/v1/transactions/{chain_id}/{hash} - Get transaction by hash

Addresses

  • GET /api/v1/addresses/{chain_id}/{address} - Get address information
  • GET /api/v1/search?q={query} - Unified search (auto-detects type: block number, address, or transaction hash)

Health

  • GET /health - Health check endpoint

Mission control

  • GET /api/v1/mission-control/stream - SSE stream for bridge/RPC health
  • GET /api/v1/mission-control/bridge/trace?tx=0x... - Blockscout-backed tx trace with Chain 138 contract labels
  • GET /api/v1/mission-control/liquidity/token/{address}/pools - 30-second cached proxy to token-aggregation pools

Access and API keys

  • GET /api/v1/access/me - Current signed-in access user and subscriptions
  • GET /api/v1/access/products - RPC product catalog for Core, Alltra, and Thirdweb lanes
  • GET /api/v1/access/subscriptions - List product subscriptions
  • POST /api/v1/access/subscriptions - Request or activate a product subscription
  • GET /api/v1/access/admin/subscriptions - List pending or filtered subscriptions for admin review
  • POST /api/v1/access/admin/subscriptions - Approve, suspend, or revoke a subscription as an admin
  • GET /api/v1/access/api-keys - List issued API keys
  • POST /api/v1/access/api-keys - Create an API key for a tier, product, scopes, expiry, and optional quota override
  • POST /api/v1/access/api-keys/{id} - Revoke an API key
  • DELETE /api/v1/access/api-keys/{id} - Alternate revoke verb
  • GET /api/v1/access/usage - Per-product usage summary
  • GET /api/v1/access/audit - Recent validated API-key usage rows for the signed-in user
  • GET /api/v1/access/admin/audit - Admin view of recent validated API-key usage rows, optionally filtered by product
  • POST /api/v1/access/internal/validate-key - Internal edge validation hook for API-key enforcement and usage logging
  • GET /api/v1/access/internal/validate-key - auth_request-friendly validator for nginx or similar proxies

Track 4 operator

  • POST /api/v1/track4/operator/run-script - Run an allowlisted script under OPERATOR_SCRIPTS_ROOT

Features

  • Input validation (addresses, hashes, block numbers)
  • Pagination support
  • Query timeouts for database operations
  • CORS headers
  • Request logging
  • Error handling with consistent error format
  • Health checks with database connectivity
  • Wallet JWT auth for track endpoints
  • Email/password user sessions for the explorer access console
  • RPC product catalog, subscription state, API key issuance, revocation, and usage summaries

Running

cd backend/api/rest
go run main.go

Or use the development script:

./scripts/run-dev.sh

Configuration

Set environment variables:

  • DB_HOST - Database host
  • DB_PORT - Database port
  • DB_USER - Database user
  • DB_PASSWORD - Database password
  • DB_NAME - Database name
  • PORT - API server port (default: 8080)
  • CHAIN_ID - Chain ID (default: 138)
  • RPC_URL - Chain RPC used by Track 1 and mission-control health/SSE data
  • TOKEN_AGGREGATION_BASE_URL - Upstream token-aggregation base URL for mission-control liquidity proxy
  • BLOCKSCOUT_INTERNAL_URL - Internal Blockscout base URL for bridge trace lookups
  • EXPLORER_PUBLIC_BASE - Public explorer base URL used in mission-control trace responses
  • CCIP_RELAY_HEALTH_URL - Optional relay health probe URL, for example http://192.168.11.11:9860/healthz
  • CCIP_RELAY_HEALTH_URLS - Optional comma-separated named relay probes, for example mainnet=http://192.168.11.11:9860/healthz,bsc=http://192.168.11.11:9861/healthz,avax=http://192.168.11.11:9862/healthz
  • MISSION_CONTROL_CCIP_JSON - Optional JSON snapshot fallback when relay health is provided as a file instead of an HTTP endpoint
  • OPERATOR_SCRIPTS_ROOT - Root directory for allowlisted Track 4 scripts
  • OPERATOR_SCRIPT_ALLOWLIST - Comma-separated list of permitted script names or relative paths
  • OPERATOR_SCRIPT_TIMEOUT_SEC - Optional Track 4 script timeout in seconds (max 599)
  • JWT_SECRET - Shared secret for wallet and user-session JWT signing
  • ACCESS_ADMIN_EMAILS - Comma-separated email allowlist for access-console admins
  • ACCESS_INTERNAL_SECRET - Shared secret used by internal edge validators calling /api/v1/access/internal/validate-key

Auth model

There are now two distinct auth planes:

  1. Wallet auth

    • POST /api/v1/auth/nonce
    • POST /api/v1/auth/wallet
    • Used for wallet-oriented explorer tracks and operator features.
  2. Access-console user auth

    • POST /api/v1/auth/register
    • POST /api/v1/auth/login
    • Used for /api/v1/access/* endpoints and the frontend /access console.

RPC access model

The access layer currently models three RPC products:

  • core-rpc
    • Provider: besu-core
    • VMID: 2101
    • Approval required
    • Intended for operator-grade and sensitive use
  • alltra-rpc
    • Provider: alltra
    • VMID: 2102
    • Self-service subscription model
  • thirdweb-rpc
    • Provider: thirdweb
    • VMID: 2103
    • Self-service subscription model

The explorer can now:

  • register and authenticate users
  • publish an RPC product catalog
  • create product subscriptions
  • issue scoped API keys
  • set expiry presets and quota overrides
  • rotate keys by minting a replacement and revoking the old one
  • review approval-gated subscriptions through an admin surface
  • revoke keys
  • show usage summaries
  • show recent audit activity for users and admins
  • validate keys for internal edge enforcement and append usage records
  • support nginx auth_request integration through the GET /api/v1/access/internal/validate-key form

Current limitation:

  • the internal validation hook exists, but nginx/Besu/relay still need to call it or replicate its rules to enforce traffic at the edge
  • billing collection and invoicing are not yet handled by this package

Operational reference:

  • explorer-monorepo/deployment/ACCESS_EDGE_ENFORCEMENT_RUNBOOK.md
  • explorer-monorepo/deployment/common/nginx-rpc-api-key-gate.conf

Mission-control deployment notes

  • Include explorer-monorepo/deployment/common/nginx-mission-control-sse.conf in the same nginx server block that proxies /explorer-api/.
  • Keep the nginx upstream port aligned with the Go API PORT.
  • Verify internal reachability to BLOCKSCOUT_INTERNAL_URL and TOKEN_AGGREGATION_BASE_URL from the API host before enabling the mission-control cards in production.