Skip to content

refactor: namespace non-public endpoints under /api/internal/#455

Open
kasperpawlowski wants to merge 4 commits into
developmentfrom
refactor/namespace-internal-api
Open

refactor: namespace non-public endpoints under /api/internal/#455
kasperpawlowski wants to merge 4 commits into
developmentfrom
refactor/namespace-internal-api

Conversation

@kasperpawlowski

@kasperpawlowski kasperpawlowski commented May 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Some third-party integrators have started consuming endpoints like /api/vaults server-side, then complaining when we ship breaking changes — even though we never promised stability on anything outside /api/public/*. The codebase already special-cases /api/public/ in server/middleware/cors.ts as the stable, anyone-can-call surface. Everything else was implicitly private but unlabelled, which is what made the misuse easy.

This change makes the line both visible (URL prefix, response header, docs) and enforceable (hard 403 for non-browser cross-origin callers) across all 18 non-public endpoints in one cutover. The goal isn't to defeat a determined scraper — it's to give integrators an unambiguous signal they're outside the public contract and to make it impossible to consume the surface accidentally. See docs/PUBLIC_API.md for the new shape.

Highlights

  • Move every non-public endpoint into server/api/internal/ via Nuxt file-based routing. Includes screen-address and sentry-tunnel. Only /api/public/is-known and /api/public/metadata stay outside.
  • server/middleware/cors.ts:
    • Set X-API-Stability: internal; may-break-without-notice on every non-public response.
    • Reject no-Origin callers (curl, server-side fetch) with 403 in prod. Our own server-to-server traffic bypasses via the existing isInternalRequest() sentinel from server/utils/internal-headers.ts.
    • Cross-origin browser rejection (already there) untouched.
  • Update every internal caller: 14 client-side composables / entities / plugins, 4 server-side utilities, the body-limit middleware path checks, the warm-cache plugin's relative imports, the Sentry tunnel config, the Pyth update helpers in utils/pyth.ts, and the three tests that import handler modules by path.
  • New docs/PUBLIC_API.md listing the supported surface and the explicit no-stability promise for /api/internal/*. README cross-links it.

Test plan

  • npx vue-tsc --noEmit — clean
  • npm run test --run — 763 passed / 1 skipped / 0 failed
  • npm run dev — load the app, confirm every screen still renders (vault lists, market detail, position, lend, borrow, earn, rewards, ToS, Sentry tunnel). Network tab should show all internal calls hitting /api/internal/* and returning 200.
  • In prod-mode (DOPPLER_ENVIRONMENT=prd npm run preview or staging deploy): curl https://<deploy>/api/internal/vaults?chainId=1 (no Origin) → 403. curl -H "Origin: https://example.com" .../api/internal/vaults?chainId=1 → 403. curl https://<deploy>/api/public/is-known?... → 200.
  • Confirm response includes X-API-Stability: internal; may-break-without-notice on any /api/internal/* call.

Summary by CodeRabbit

  • Documentation

    • Clarified the stable public API surface and updated CORS/troubleshooting guidance.
  • Chores

    • Routed many backend data fetches through internal proxy endpoints to centralize server behavior (no user-facing changes).
    • Updated troubleshooting to rely on the server-side token list for missing logos.

Review Change Stack

Some third-party integrators have started consuming endpoints like
/api/vaults server-side and then complaining when we ship breaking
changes. We never promised stability on anything outside /api/public/*
— but with no namespace separation, the contract was easy to miss.

This makes the line visible and enforceable.

Changes:
- Move all 18 non-public endpoints into /api/internal/* via file-based
  routing. Includes /api/screen-address and /api/sentry-tunnel. Only
  /api/public/is-known and /api/public/metadata stay outside the
  namespace.
- server/middleware/cors.ts hard-blocks any non-internal caller hitting
  /api/internal/* without an Origin header (curl, server-side fetch,
  etc.) with 403. Our own server-to-server traffic bypasses via the
  existing isInternalRequest() sentinel set by INTERNAL_FETCH_HEADERS.
  Cross-origin browser callers already 403 via the existing allow-list
  check; that's unchanged.
- Add X-API-Stability: internal; may-break-without-notice on every
  non-public response so anyone reading their network tab gets the
  signal even if they ignore the URL prefix.
- Add docs/PUBLIC_API.md listing the supported public surface and
  spelling out the internal namespace's lack of stability guarantees.
  README cross-links it.
- Update every client and server caller: composables, entities,
  plugins, server-side utilities, body-limit middleware, warm-cache
  imports, sentry tunnel config, Pyth update fetch helpers in
  utils/pyth.ts, and tests that import handler modules by path.
@railway-app

railway-app Bot commented May 15, 2026

Copy link
Copy Markdown

🚅 Deployed to the euler-lite-pr-455 environment in euler-lite

Service Status Web Updated (UTC)
production-master-branch ✅ Success (View Logs) Web May 15, 2026 at 5:46 pm

@railway-app railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-455 May 15, 2026 17:39 Destroyed
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: euler-xyz/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0a6c4f96-8d58-49ee-95e5-de70ffa42f3d

📥 Commits

Reviewing files that changed from the base of the PR and between ddd863c and 8a97c5b.

📒 Files selected for processing (9)
  • composables/useBrevis.ts
  • composables/useEulerAddresses.ts
  • composables/useFuul.ts
  • composables/useMerkl.ts
  • server/api/internal/screen-address.post.ts
  • server/middleware/cors.ts
  • services/trm.ts
  • tests/server/screen-address.test.ts
  • utils/public-client.ts
✅ Files skipped from review due to trivial changes (1)
  • composables/useBrevis.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • services/trm.ts
  • composables/useFuul.ts
  • composables/useEulerAddresses.ts
  • server/middleware/cors.ts
  • composables/useMerkl.ts

📝 Walkthrough

Walkthrough

Migrates callers, server utilities, tests, and docs from public /api/* to /api/internal/*, adds docs/PUBLIC_API.md declaring /api/public/* as the stable surface, and enforces request checks plus an X-API-Stability header in the CORS middleware.

Changes

API Endpoint Internalization

Layer / File(s) Summary
Public API contract and README
docs/PUBLIC_API.md, README.md
Adds docs/PUBLIC_API.md listing stable /api/public/* endpoints and updates README environment/docs to reference internal proxy paths.
CORS middleware and API stability headers
server/middleware/cors.ts
Imports isInternalRequest, sets X-API-Stability: internal; may-break-without-notice for non-/api/public/* routes, and adds a prod-only guard rejecting internal requests lacking Origin unless internal.
Body limit middleware for internal endpoints
server/middleware/body-limit.ts
Applies request body size limits to /api/internal/tenderly/, /api/internal/rpc/, and /api/internal/sentry-tunnel.
Client-side composables and client config updates
composables/*.ts, plugins/00.wagmi.ts, sentry.client.config.ts, utils/pyth.ts, utils/public-client.ts
Updates client-side fetches and configs to use /api/internal/* equivalents (rewards proxies, RPC, Tenderly, token list, TOS, vaults, Euler chains/labels/oracle adapters, intrinsic APY, Pyth updates, Sentry tunnel); public-client injects server-only sentinel headers.
Server utilities, caches, and entity factory updates
server/utils/*, entities/vault/*, server/utils/vaults-cache.ts, plugins/00.wagmi.ts
Server-side utilities and caches now fetch chain config, labels, token lists, and vault categories from /api/internal/*; entity factory and loader comments updated accordingly.
Warm-cache, TRM service, README troubleshooting
server/plugins/warm-cache.ts, services/trm.ts, README.md
Warm-cache imports internal label handlers, TRM routes screenAddress to /api/internal/screen-address, README troubleshooting now references /api/internal/token-list.
Test imports and path updates
tests/server/*.test.ts
Tests updated to import handlers and helpers from internal API modules and to stub internal request paths for rewards, Sentry tunnel, screen-address, and label validation; assertions unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • euler-xyz/euler-lite#268: Introduced backend surfaces for vault categorization, rewards proxies, and token list referenced by this PR.
  • euler-xyz/euler-lite#112: Added server caching/proxy endpoints (Euler chains, labels, adapters) that client/server code now routes to via /api/internal/*.
  • euler-xyz/euler-lite#258: Related CORS/public-endpoint work intersecting with the new public/internal request handling.

Suggested reviewers

  • Seranged

"A rabbit hops through code and docs so spry,
Internal paths now hidden from the sky,
Public API marked true,
Middleware checks see you through,
Hooray for tidy proxies, hop! 🐇"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main refactoring: moving non-public API endpoints under /api/internal/ namespace, which is the primary objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/namespace-internal-api

Comment @coderabbitai help to get the list of available commands and usage tips.

…pace-internal-api

# Conflicts:
#	composables/useMerkl.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/trm.ts (1)

8-15: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Check response status before parsing to ensure proper error fallback.

The code currently skips the error handler on non-2xx responses. If the API returns 403/500 with valid JSON (even an error object), the code parses it and returns Boolean(data?.addressIsSuspicious) which resolves to false—the opposite of the safe fallback. The catch block's return true never executes because no exception occurs. Add a status check to trigger the safe fallback:

Suggested fix
  try {
    const resp = await fetch('/api/internal/screen-address', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ address, vpnIsUsed }),
    })
+   if (!resp.ok) throw new Error(`screen-address failed with status ${resp.status}`)
 
    const data = await resp.json()
    return Boolean(data?.addressIsSuspicious)
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/trm.ts` around lines 8 - 15, Update the fetch handling for
'/api/internal/screen-address' to treat non-2xx responses as errors before
parsing: after awaiting resp from fetch, check resp.ok (or resp.status) and if
it's false, trigger the safe fallback (e.g., throw or directly return true) so
the catch block or fallback path runs instead of parsing error JSON; then only
call resp.json() and return Boolean(data?.addressIsSuspicious) when resp.ok is
true. Ensure this change is applied around the resp and data usage in
services/trm.ts so the API error cases produce the safe true result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@composables/useRpcClient/index.ts`:
- Around line 16-18: The server-side RPC proxy fails in production because viem
HTTP transports created by getPublicClient(...) lack the INTERNAL_FETCH_HEADERS
sentinel, so update getPublicClient in utils/public-client.ts to accept an
optional headers?: Record<string,string> parameter and pass it into the
http(...) transport options (ensuring the cache key accounts for headers, e.g.
rpcUrl + JSON.stringify(headers) to avoid returning a client with wrong
headers); then update the composable in composables/useRpcClient/index.ts to
call getPublicClient(rpcUrl, INTERNAL_FETCH_HEADERS) when import.meta.server is
true (and call it without headers on the client).

In `@server/middleware/cors.ts`:
- Around line 118-121: The statusMessage passed to createError in the middleware
(the createError(...) call that currently uses 'Forbidden — /api/internal/* is
not a public contract. See docs/PUBLIC_API.md.') contains a non-ASCII em dash;
replace it with an ASCII-safe short statusMessage (e.g., 'Forbidden -
/api/internal/* is not a public contract') or simpler 'Forbidden', and move the
detailed text (including the docs/PUBLIC_API.md reference) into the error's
message/body field so all non-ASCII characters are removed from statusMessage
while preserving the full explanation in the response payload.

---

Outside diff comments:
In `@services/trm.ts`:
- Around line 8-15: Update the fetch handling for '/api/internal/screen-address'
to treat non-2xx responses as errors before parsing: after awaiting resp from
fetch, check resp.ok (or resp.status) and if it's false, trigger the safe
fallback (e.g., throw or directly return true) so the catch block or fallback
path runs instead of parsing error JSON; then only call resp.json() and return
Boolean(data?.addressIsSuspicious) when resp.ok is true. Ensure this change is
applied around the resp and data usage in services/trm.ts so the API error cases
produce the safe true result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: euler-xyz/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c8da321f-ac31-4297-941d-4e17e1b6a3a7

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe3199 and 3aff10b.

📒 Files selected for processing (50)
  • README.md
  • composables/useAccountPositions.ts
  • composables/useBrevis.ts
  • composables/useEulerAddresses.ts
  • composables/useEulerLabels.ts
  • composables/useFuul.ts
  • composables/useIntrinsicApy.ts
  • composables/useMerkl.ts
  • composables/useRpcClient/index.ts
  • composables/useTenderlySimulation.ts
  • composables/useTokenList.ts
  • composables/useTosData.ts
  • composables/useVaults.ts
  • docs/PUBLIC_API.md
  • entities/vault/factory.ts
  • entities/vault/loader.ts
  • plugins/00.wagmi.ts
  • sentry.client.config.ts
  • server/api/internal/euler-chains.get.ts
  • server/api/internal/intrinsic-apy.get.ts
  • server/api/internal/labels/[file].get.ts
  • server/api/internal/oracle-adapter.get.ts
  • server/api/internal/oracle-adapters.get.ts
  • server/api/internal/pyth/updates.get.ts
  • server/api/internal/rewards/brevis.get.ts
  • server/api/internal/rewards/fuul.get.ts
  • server/api/internal/rewards/merkl.get.ts
  • server/api/internal/rpc/[chainId].ts
  • server/api/internal/screen-address.post.ts
  • server/api/internal/sentry-tunnel.post.ts
  • server/api/internal/tenderly/simulate.post.ts
  • server/api/internal/tenderly/status.get.ts
  • server/api/internal/token-list.get.ts
  • server/api/internal/tos.get.ts
  • server/api/internal/vault-categories.get.ts
  • server/api/internal/vaults.get.ts
  • server/middleware/body-limit.ts
  • server/middleware/cors.ts
  • server/plugins/warm-cache.ts
  • server/utils/escrow-perspective.ts
  • server/utils/labels-helpers.ts
  • server/utils/labels-view.ts
  • server/utils/vault-categories-store.ts
  • server/utils/vaults-cache.ts
  • services/trm.ts
  • tests/server/internal-request.test.ts
  • tests/server/labels-validate-node.test.ts
  • tests/server/rewards-handlers.test.ts
  • tests/server/sentry-tunnel.test.ts
  • utils/pyth.ts

Comment thread composables/useRpcClient/index.ts
Comment thread server/middleware/cors.ts
- Drop em dash from Forbidden statusMessage (HTTP status-line is
  ASCII-only; h3/Nitro sanitize non-ASCII), move the detail string into
  the error data payload.
- Inject the loopback cf-connecting-ip sentinel on server-side viem HTTP
  transports so /api/internal/rpc/* calls originating inside the Nuxt
  process pass the new CORS internal-only check in production.
- Update screen-address test import to the new /api/internal location
  picked up from development.
@kasperpawlowski kasperpawlowski requested a review from Seranged May 19, 2026 23:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant