65c57a2246060532a4d464382b2c9d72fa66ab29
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.'
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
- ✅ Tests database connection
- ✅ Runs migration (if needed)
- ✅ Stops existing server
- ✅ Starts server with database
- ✅ Tests all endpoints
- ✅ 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 fromfrontend/withnpm run buildand 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 usingdeployment/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.shand./scripts/deploy.shnow 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 indexdocs/EXPLORER_API_ACCESS.md— API access, 502 fix, CSP, frontend deploySTART_HERE.md— Quick start with all commandsCOMPLETE_DEPLOYMENT.md— Detailed deployment stepsDEPLOYMENT_COMPLETE_FINAL.md— Final status reportREADME_DEPLOYMENT.md— Deployment quick referencedeployment/DEPLOYMENT_GUIDE.md— Full LXC/Nginx/Cloudflare deployment guidedocs/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:
L@ker$2010 - 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— backendgo test ./...and frontendnpm 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 buildornpm test— Next.js build (includes lint) or lint + type-check only. - E2E:
make test-e2eornpm run e2efrom repo root — Playwright tests against https://blockscout.defi-oracle.io by default; useEXPLORER_URL=http://localhost:3000for local.
Status
✅ All implementation complete
✅ All scripts ready
✅ All documentation complete
✅ Frontend: C1–C4, M1–M4, 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
Languages
Shell
32.1%
TypeScript
26.6%
Go
20%
JavaScript
16.4%
HTML
4.4%
Other
0.4%