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
2026-04-16 11:12:17 -07:00

SolaceScan Explorer - Tiered Architecture

🚀 Quick Start - Complete Deployment

Execute this single command to complete all deployment steps:

cd ~/projects/proxmox/explorer-monorepo
bash EXECUTE_DEPLOYMENT.sh

What This Does

  1. Tests database connection
  2. Runs migration (if needed)
  3. Stops existing server
  4. Starts server with database
  5. Tests all endpoints
  6. Provides status summary

Manual Execution

If the script doesn't work, see START_HERE.md for step-by-step manual commands.

Frontend

  • Production (canonical target): the current Next.js standalone frontend in frontend/src/, built from frontend/ with npm run build and deployed to VMID 5000 as a Node service behind nginx.
  • Canonical deploy script: ./scripts/deploy-next-frontend-to-vmid5000.sh
  • Canonical nginx wiring: keep /api, /api/config/*, /explorer-api/*, /token-aggregation/api/v1/*, /snap/, and /health; proxy / and /_next/ to the frontend service using deployment/common/nginx-next-frontend-proxy.conf.
  • Legacy fallback only: the static SPA (frontend/public/index.html + explorer-spa.js) remains in-repo for compatibility/reference, but it is not a supported primary deployment target.
  • Architecture command center: frontend/public/chain138-command-center.html — tabbed Mermaid topology (Chain 138 hub, network, stack, flows, cross-chain, cW Mainnet, off-chain, integrations). Linked from the SPA More → Explore → Visual Command Center.
  • Legacy static deploy scripts: ./scripts/deploy-frontend-to-vmid5000.sh and ./scripts/deploy.sh now fail fast with a deprecation message and point to the canonical Next.js deploy path.
  • Frontend review & tasks: frontend/FRONTEND_REVIEW.md, frontend/FRONTEND_TASKS_AND_REVIEW.md

Documentation

  • docs/README.md — Documentation overview and index
  • docs/EXPLORER_API_ACCESS.md — API access, 502 fix, CSP, frontend deploy
  • START_HERE.md — Quick start with all commands
  • COMPLETE_DEPLOYMENT.md — Detailed deployment steps
  • DEPLOYMENT_COMPLETE_FINAL.md — Final status report
  • README_DEPLOYMENT.md — Deployment quick reference
  • deployment/DEPLOYMENT_GUIDE.md — Full LXC/Nginx/Cloudflare deployment guide
  • docs/INDEX.md — Bridge and operations doc index

Architecture

  • Track 1 (Public): RPC Gateway - No authentication required
  • Track 2 (Approved): Indexed Explorer - Requires authentication
  • Track 3 (Analytics): Analytics Dashboard - Requires Track 3+
  • Track 4 (Operator): Operator Tools - Requires Track 4 + IP whitelist

Configuration

  • Database User: explorer
  • Database Password: ***REDACTED-LEGACY-PW***
  • RPC URL: http://192.168.11.250:8545
  • Chain ID: 138
  • Port: 8080

Reusable libs (extraction)

Reusable components live under backend/libs/ and frontend/libs/ and may be split into separate repos and linked via git submodules. Clone with submodules:

git clone --recurse-submodules <repo-url>
# or after clone:
git submodule update --init --recursive

See docs/REUSABLE_COMPONENTS_EXTRACTION_PLAN.md for the full plan.

Testing

  • All unit/lint: make test — backend go test ./... and frontend npm test (lint + type-check).
  • Backend: cd backend && go test ./... — API tests run without a real DB; health returns 200 or 503, DB-dependent endpoints return 503 when DB is nil.
  • Frontend: cd frontend && npm run build or npm test — Next.js build (includes lint) or lint + type-check only.
  • E2E: make test-e2e or npm run e2e from repo root — Playwright tests against https://blockscout.defi-oracle.io by default; use EXPLORER_URL=http://localhost:3000 for local.

Status

All implementation complete
All scripts ready
All documentation complete
Frontend: C1C4, M1M4, H4, H5, L2, L4 done; H1/H2/H3 (escapeHtml/safe href) in place; optional L1, L3 remain
CI: backend + frontend tests; lint job runs go vet, npm run lint, npm run type-check
Tests: make test, make test-e2e, make build all pass

Ready for deployment!

Description
No description provided
Readme 50 MiB
Languages
Shell 32.1%
TypeScript 26.6%
Go 20%
JavaScript 16.4%
HTML 4.4%
Other 0.4%