Publish walletconnect config endpoints, Track 3/4 notes on analytics/operator pages, legacy SPA at /legacy/index.html with root redirect, and a parity verifier for explorer.d-bis.org vs blockscout.defi-oracle.io.
Co-authored-by: Cursor <cursoragent@cursor.com>
Operations pages get collapsible surface navigation on small screens and a shared action-card accordion; the footer surfaces read-only JSON endpoints with e2e coverage.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move extended token-list label to the operations intro, wait for network idle
before asserting, and clear conflicting NO_COLOR/FORCE_COLOR in Playwright config.
Co-authored-by: Cursor <cursoragent@cursor.com>
Unify wallet/catalog/extended token-list policy, add contract verification CTA,
trim the homepage dashboard with status strip and recent activity, and add Playwright smoke coverage.
Co-authored-by: Cursor <cursoragent@cursor.com>
The health check stopped after two non-empty lines and missed the data line that follows event: ping on mission-control streams.
Co-authored-by: Cursor <cursoragent@cursor.com>
Align wallet SSR with report token-list, dedupe featured v1 tokens, refresh home and wallet snapshots on a 60s cadence, and drive vanilla SPA chain add/watch from API metadata. Add shared pagination/tabs for address, token, and transaction pages, extend token aggregation helpers, and harden stats API with tests and health checks.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add TokenSigningSurfaceCard: ABI flags, eip712Domain eth_call decode, verification metadata
- Pass contract profile into GRU standards detection on token page
- Table layout=tabular for Top Holders column layout at all breakpoints
- Fallback provenance name/symbol; show signing card when token API empty
- eip712Domain.ts: decode ERC-5267 tuple return data
Co-authored-by: Cursor <cursoragent@cursor.com>
Corrections per 2026-04 institutional review:
- MLFO reclassified as Global Family Office (was incorrectly labeled central bank)
- BIS Innovation Hub reclassified as Standards Body (does not hold observer seat)
- Added missing entities: ICCC, SAID, PANDA, Order of Hospitallers (XOM)
- Added BRICS founding + expanded member central banks (10 entries)
New institutional tier taxonomy (7 tiers):
sovereign_central_bank, global_family_office, settlement_member,
infrastructure_operator, oversight_judicial, delegated_authority,
standards_body
Backend changes:
- New auth/membership.go: tier types, DefaultTrackForTier mapping,
MembershipStore with DB queries for member directory
- New migration 0017: institutional_members + institutional_member_wallets
tables with seed data for all corrected members
- Updated wallet_auth.go getUserTrack(): now resolves institutional
membership (via wallet junction table) before defaulting to Track 1
- WalletAuthResponse now includes institutional_tier and institution_name
- New REST endpoints: GET /api/v1/membership/{tiers,members,members/:slug}
- Added TrackLabel() helper in featureflags
Frontend changes:
- Added InstitutionalTier type and label map to access.ts
- WalletAccessSession extended with institutionalTier/institutionName
- Navbar getAccessTier() now displays institutional tier label when present
- Session summary shows institution name
Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
Two small follow-ups to the out-of-band git-history rewrite that
purged L@ker$2010 / L@kers2010 / L@ker\$2010 from every branch and
tag:
.gitleaks.toml:
- Regex was L@kers?\$?2010 which catches the expanded form but
NOT the shell-escaped form (L@ker\$2010) that slipped past PR #3
in scripts/setup-database.sh. PR #13 fixed the live leak but did
not tighten the detector. New regex L@kers?\\?\$?2010 catches
both forms so future pastes of either form fail CI.
- Description rewritten without the literal password (the previous
description was redacted by the history rewrite itself and read
'Legacy hardcoded ... (***REDACTED-LEGACY-PW*** / ***REDACTED-LEGACY-PW***)'
which was cryptic).
docs/SECURITY.md:
- New 'History-purge audit trail' section recording what was done,
how it was verified (0 literal password matches in any blob or
commit message; 0 legacy-password findings from a post-rewrite
gitleaks scan), and what operator cleanup is still required on
the Gitea host to drop the 13 refs/pull/*/head refs that still
pin the pre-rewrite commits (the update hook declined those refs
over HTTPS, so only an admin on the Gitea VM can purge them via
'git update-ref -d' + 'git gc --prune=now' in the bare repo).
- New 'Re-introduction guard' subsection pointing at the tightened
regex and commit 78e1ff5.
Verification:
gitleaks detect --no-git --source . --config .gitleaks.toml # 0 legacy hits
git log --all -p | grep -cE 'L@ker\$2010|L@kers2010' # 0
PR #3 scrubbed ***REDACTED-LEGACY-PW*** from every env file, compose unit, and
deployment doc but missed scripts/setup-database.sh, which still hard-
coded DB_PASSWORD="***REDACTED-LEGACY-PW***" on line 17. That slipped past
gitleaks because the shell-escaped form (backslash-dollar) does not
match the L@kers?\$?2010 regex committed in .gitleaks.toml -- the
regex was written to catch the *expanded* form, not the source form.
This commit removes the hardcoded default and requires DB_PASSWORD to
be exported by the operator before running the script. Same pattern as
the rest of the PR #3 conversion (fail-fast at boot when a required
secret is unset) so there is no longer any legitimate reason for the
password string to live in the repo.
Verification:
git grep -nE 'L@kers?\\?\$?2010' -- scripts/ # no matches
bash -n scripts/setup-database.sh # clean
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.
Replaces an 89-line README that mostly duplicated code links with a
90-line README that answers the three questions a new reader actually
asks: 'what is this?', 'how do I run it?', 'where do I go next?'.
Also adds two longer-form references that the old README was missing
entirely:
docs/ARCHITECTURE.md (new):
- Four Mermaid diagrams:
1. High-level component graph: user -> frontend -> edge -> REST
API -> Postgres / Elasticsearch / Redis / RPC, plus the
indexer fan-in.
2. Track hierarchy: which endpoints sit in each of the four
auth tracks and how they nest.
3. Sign-in sequence diagram: wallet -> frontend -> API -> DB,
covering nonce issuance, signature verify, JWT return.
4. Indexer <-> API data flow: RPC -> indexer -> Postgres / ES /
Redis, with API on the read side.
- Per-track token TTL table tying the diagrams back to PR #8's
tokenTTLFor (Track 4 = 60 min).
- Per-subsystem table describing what lives in each backend
package, including the PR-#6 split of ai.go into six files.
- Runtime dependencies table.
- Security posture summary referencing PR #3's fail-fast JWT /
CSP checks, .gitleaks.toml, and docs/SECURITY.md.
docs/API.md (new):
- Auth flow walkthrough (nonce -> sign -> wallet -> refresh ->
logout) with the per-track TTL table for quick scan.
- Rate-limit matrix.
- Tagged endpoint index generated from
backend/api/rest/swagger.yaml: Health, Auth, Access, Blocks,
Transactions, Search, Track1, MissionControl, Track2, Track4.
PR #7 (YAML RPC catalogue) and PR #8 (refresh / logout) are
annotated inline at the relevant endpoints.
- Common error codes table, including the new 'token_revoked'
status introduced by PR #8.
- Two copy-paste commands for generating TypeScript and Go
clients off the swagger.yaml, so downstream repos don't have
to hand-maintain one.
README.md:
- Trimmed to 90 lines (previous was 89 lines of README lore).
- Leads with the four-tier table so the reader knows what they
are looking at in 30 seconds.
- 'Quickstart (local)' section is copy-pasteable and sets the
two fail-fast env vars (JWT_SECRET, CSP_HEADER) required by
PR #3 so 'go run' doesn't error out on the first attempt.
- Forward-references docs/ARCHITECTURE.md, docs/API.md,
docs/TESTING.md (from PR #10), docs/SECURITY.md (from PR #3),
and CONTRIBUTING.md.
- Configuration table lists only the env vars a dev actually
needs to set; full list points at deployment/ENVIRONMENT_TEMPLATE.env.
Verification:
wc -l README.md = 93 (target was <=150).
wc -l docs/ARCHITECTURE.md = 145 (four diagrams, tables, pointers).
wc -l docs/API.md = 115 (index + auth/error tables).
markdownlint-style scan no obvious issues.
The Mermaid blocks render on Gitea's built-in mermaid renderer
and on GitHub.
Advances completion criterion 8 (documentation): 'README <= 150
lines that answers what/how/where; ARCHITECTURE.md with diagrams
of tracks, components, and data flow; API.md generated from
swagger.yaml. Old ~300 status markdown files were removed by PR #2.'
Closes the 'e2e tests only hit production; no local full-stack harness'
finding from the review. The existing e2e suite
(scripts/e2e-explorer-frontend.spec.ts) runs against explorer.d-bis.org
and so can't validate a PR before it merges -- it's a production canary,
not a pre-merge gate.
This PR adds a parallel harness that stands the entire stack up locally
(postgres + elasticsearch + redis via docker-compose, backend API, and
a production build of the frontend) and runs a Playwright smoke spec
against it. It is wired into Make and into a dedicated CI workflow.
Changes:
scripts/e2e-full.sh (new, chmod +x):
- docker compose -p explorer-e2e up -d postgres elasticsearch redis.
- Waits for postgres readiness (pg_isready loop).
- Runs database/migrations/migrate.go so schema + seeds including
the new 0016_jwt_revocations table from PR #8 are applied.
- Starts 'go run ./backend/api/rest' on :8080; waits for /healthz.
- Builds + starts 'npm run start' on :3000; waits for a 200.
- npx playwright install --with-deps chromium; runs the full-stack
spec; tears down docker and kills the backend+frontend processes
via an EXIT trap. E2E_KEEP_STACK=1 bypasses teardown for
interactive debugging.
- Generates an ephemeral JWT_SECRET per run so stale tokens don't
bleed across runs (and the fail-fast check from PR #3 passes).
- Provides a dev-safe CSP_HEADER default so PR #3's hardened
production CSP check doesn't reject localhost connections.
scripts/e2e-full-stack.spec.ts (new):
- Playwright spec that exercises public routes + a couple of
backend endpoints. Takes a full-page screenshot of each route
into test-results/screenshots/<route>.png so reviewers can
eyeball the render from CI artefacts.
- Covers: /healthz, /, /blocks, /transactions, /addresses, /tokens,
/pools, /search, /wallet, /routes, /api/v1/access/products (YAML
catalogue from PR #7), /api/v1/auth/nonce (SIWE kickoff).
- Sticks to Track-1 (no wallet auth needed) so it can run in CI
without provisioning a test wallet.
playwright.config.ts:
- Broadened testMatch from a single filename to /e2e-.*\.spec\.ts/
so the new spec is picked up alongside the existing production
canary spec. fullyParallel, worker, timeout, reporter, and
project configuration unchanged.
Makefile:
- New 'e2e-full' target -> ./scripts/e2e-full.sh. Listed in 'help'.
- test-e2e (production canary) left untouched.
.github/workflows/e2e-full.yml (new):
- Dedicated workflow, NOT on every push/PR (the full stack takes
minutes and requires docker). Triggers:
* workflow_dispatch (manual)
* PRs labelled run-e2e-full (opt-in for changes that touch
migrations, auth, or routing)
* nightly schedule (04:00 UTC)
- Uses Go 1.23.x and Node 20 to match PR #5's pinning.
- Uploads two artefacts on every run: e2e-screenshots
(test-results/screenshots/) and playwright-report.
docs/TESTING.md (new):
- Four-tier test pyramid: unit -> static analysis -> production
canary -> full-stack Playwright.
- Env var reference table for e2e-full.sh.
- How to trigger the CI workflow.
Verification:
bash -n scripts/e2e-full.sh clean
The spec imports compile cleanly against the existing @playwright
/test v1.40 declared in the root package.json; no new runtime
dependencies are added.
Existing scripts/e2e-explorer-frontend.spec.ts still matched by
the broadened testMatch regex.
Advances completion criterion 7 (end-to-end coverage): 'make e2e-full
boots the real stack, Playwright runs against it, CI uploads
screenshots, a nightly job catches regressions that only show up
when all services are live.'
Fixes the 'unfinished router migration + inconsistent packageManager'
finding from the review:
1. src/app/ only ever contained globals.css; every actual route lives
under src/pages/. Keeping both routers in the tree made the build
surface area ambiguous and left a trap where a future contributor
might add a new route under src/app/ and break Next's routing
resolution. PR #9 commits to the pages router and removes src/app/.
2. globals.css moved from src/app/globals.css to src/styles/globals.css
(so it no longer sits under an otherwise-deleted app router folder)
and _app.tsx's import was updated accordingly. This is a no-op at
runtime: the CSS payload is byte-identical.
3. tailwind.config.js had './src/app/**/*.{js,ts,jsx,tsx,mdx}' at the
top of its content glob list. Replaced with './src/styles/**/*.css'
so Tailwind still sees globals.css; the src/components/** and
src/pages/** globs are unchanged.
4. Unified the package manager on npm:
- package.json packageManager: 'pnpm@10.0.0' -> 'npm@10.8.2'.
The lockfile (package-lock.json) and CI (npm ci / npm run lint /
npm run type-check / npm run build in .github/workflows/ci.yml)
have always used npm; the pnpm declaration was aspirational and
would have forced contributors with corepack enabled into a tool
the repo doesn't actually support.
- Added an 'engines' block pinning node >=20 <21 and npm >=10 so
CI, Docker, and a fresh laptop clone all land on the same runtime.
Verification:
npm ci 465 packages, no warnings.
npm run lint next lint: No ESLint warnings or errors.
npm run type-check tsc --noEmit: clean.
npm run build Next.js 14.2.35 compiled 19 pages successfully;
every route (/, /blocks, /transactions, /tokens,
/bridge, /analytics, /operator, /docs, /wallet,
etc.) rendered without emitting a warning.
Advances completion criterion 5 (frontend housekeeping): 'one router;
one package manager; build is reproducible from the lockfile.'
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.'