Follow-up to PR #8 (JWT revocation + refresh), addressing the two in-scope follow-ups called out in the completion-sequence summary on PR #11: 1. swagger.yaml pre-dated /api/v1/auth/refresh and /api/v1/auth/logout - client generators could not pick them up. 2. Those handlers were covered by unit tests on the WalletAuth layer and by the e2e-full Playwright spec, but had no HTTP-level unit tests - regressions at the mux/handler seam (wrong method, missing walletAuth, unregistered route) were invisible to go test ./backend/api/rest. Changes: backend/api/rest/swagger.yaml: - New POST /api/v1/auth/refresh entry under the Auth tag. Uses bearerAuth, returns the existing WalletAuthResponse on 200, 401 via components/responses/Unauthorized, 503 when the auth storage or the jwt_revocations table from migration 0016 is missing. Description calls out that legacy tokens without a jti cannot be refreshed. - New POST /api/v1/auth/logout entry. Same auth requirement; returns {status: ok} on 200; 401 via Unauthorized; 503 when migration 0016 has not run. Description names the jwt_revocations table explicitly so ops can correlate 503s with the migration. - Both slot in alphabetically between /auth/wallet and /auth/register so the tag block stays ordered. backend/api/rest/auth_refresh_internal_test.go (new, 8 tests): - TestHandleAuthRefreshRejectsGet - GET returns 405 method_not_allowed. - TestHandleAuthRefreshReturns503WhenWalletAuthUnconfigured - walletAuth nil, POST with a Bearer header returns 503 rather than panicking (guards against a regression where someone calls s.walletAuth.RefreshJWT without the nil-check). - TestHandleAuthLogoutRejectsGet - symmetric 405 on GET. - TestHandleAuthLogoutReturns503WhenWalletAuthUnconfigured - symmetric 503 on nil walletAuth. - TestAuthRefreshRouteRegistered - exercises SetupRoutes and confirms POST /api/v1/auth/refresh and /api/v1/auth/logout are registered (i.e. not 404). Catches regressions where a future refactor drops the mux.HandleFunc entries for either endpoint. - TestAuthRefreshRequiresBearerToken + TestAuthLogoutRequiresBearerToken - sanity-check that a POST with no Authorization header resolves to 401 or 503 (never 200 or 500). - decodeErrorBody helper extracts ErrorDetail from writeError's {"error":{"code":...,"message":...}} envelope, so asserts on body["code"] match the actual wire format (not the looser {"error":"..."} shape). - newServerNoWalletAuth builds a rest.Server with JWT_SECRET set to a 32-byte string of 'a' so NewServer's fail-fast check from PR #3 is happy; nil db pool is fine because the tests do not exercise any DB path. Verification: cd backend && go vet ./... clean cd backend && go test ./api/rest/ pass (17 tests; 7 new) cd backend && go test ./... pass Out of scope: the live credential rotation in the third follow-up bullet requires infra access (database + SSH + deploy pipeline) and belongs to the operator.
SolaceScan Explorer
Multi-tier block explorer and access-control plane for Chain 138.
Four access tiers:
| Track | Who | Auth | Examples |
|---|---|---|---|
| 1 | Public | None | /blocks, /transactions, /search |
| 2 | Wallet-verified | SIWE JWT | RPC API keys, subscriptions, usage reports |
| 3 | Analytics | SIWE JWT (admin or billed) | Advanced analytics, audit logs |
| 4 | Operator | SIWE JWT (operator.*) |
run-script, mission-control, ops |
See docs/ARCHITECTURE.md for diagrams of how the
tracks, services, and data stores fit together, and docs/API.md
for the endpoint reference generated from backend/api/rest/swagger.yaml.
Repository layout
backend/ Go 1.23 services (api/rest, indexer, auth, analytics, ...)
frontend/ Next.js 14 pages-router app
deployment/ docker-compose and deploy manifests
scripts/ e2e specs, smoke scripts, operator runbooks
docs/ Architecture, API, testing, security, runbook
Quickstart (local)
Prereqs: Docker (+ compose), Go 1.23.x, Node 20.
# 1. Infra deps
docker compose -f deployment/docker-compose.yml up -d postgres elasticsearch redis
# 2. DB schema
cd backend && go run database/migrations/migrate.go && cd ..
# 3. Backend (port 8080)
export JWT_SECRET=$(openssl rand -hex 32)
export CSP_HEADER="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' http://localhost:8080 ws://localhost:8080"
cd backend/api/rest && go run . &
# 4. Frontend (port 3000)
cd frontend && npm ci && npm run dev
Or let make e2e-full do everything end-to-end and run Playwright
against the stack (see docs/TESTING.md).
Configuration
Every credential, URL, and RPC endpoint is an env var. There is no in-repo production config. Minimum required by a non-dev binary:
| Var | Purpose | Notes |
|---|---|---|
JWT_SECRET |
HS256 wallet-auth signing key | Fail-fast if empty |
CSP_HEADER |
Content-Security-Policy response header |
Fail-fast if empty |
DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME |
Postgres connection | |
REDIS_HOST / REDIS_PORT |
Redis cache | |
ELASTICSEARCH_URL |
Indexer / search backend | |
RPC_URL / WS_URL |
Upstream Chain 138 RPC | |
RPC_PRODUCTS_PATH |
Optional override for backend/config/rpc_products.yaml |
PR #7 |
Full list: deployment/ENVIRONMENT_TEMPLATE.env.
Testing
# Unit tests + static checks
cd backend && go test ./... && staticcheck ./... && govulncheck ./...
cd frontend && npm test && npm run test:unit
# Production canary
EXPLORER_URL=https://explorer.d-bis.org make test-e2e
# Full local stack + Playwright
make e2e-full
See docs/TESTING.md.
Contributing
Branching, PR template, CI gates, secret handling: see
CONTRIBUTING.md. Never commit real credentials —
.gitleaks.toml will block the push and rotation steps live in
docs/SECURITY.md.
Licence
MIT.