Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Euler Lite provides all the core functionality of Euler Finance in a customizabl
- **Rewards**: Participate in Merkl, Incentra, and Fuul reward programs
- **Multi-chain Support**: Connect to multiple EVM-compatible networks

> The only API surface this app commits to keeping stable is documented in [docs/PUBLIC_API.md](docs/PUBLIC_API.md). Anything else under `/api/internal/*` is implementation detail.

## Prerequisites

- **Node.js** 24+ (recommended: 24.14.1)
Expand Down Expand Up @@ -49,7 +51,7 @@ cp .env.example .env
| ----------------- | ----------------------------- | ------------------------------------- |
| `EULER_API_URL` | — | Euler API (tokens, prices, logos) |
| `SWAP_API_URL` | — | Euler swap API |
| `PYTH_HERMES_URL` | `https://hermes.pyth.network` | Pyth oracle endpoint (server-only, proxied via `/api/pyth/updates`) |
| `PYTH_HERMES_URL` | `https://hermes.pyth.network` | Pyth oracle endpoint (server-only, proxied via `/api/internal/pyth/updates`) |

> **Doppler compatibility:** If your secret manager injects `NUXT_PUBLIC_*` prefixed names (e.g. `NUXT_PUBLIC_EULER_API_URL`), the app accepts both forms automatically.

Expand Down Expand Up @@ -332,7 +334,7 @@ Before deploying:
### Token logos not loading

- Verify `EULER_API_URL` is set correctly. If using Doppler, ensure the env var name matches (`EULER_API_URL` or `NUXT_PUBLIC_EULER_API_URL`).
- Token data is fetched server-side via `/api/token-list` which aggregates Euler API, DefiLlama, and Uniswap sources with fallback. Check server logs for upstream failures.
- Token data is fetched server-side via `/api/internal/token-list` which aggregates Euler API, DefiLlama, and Uniswap sources with fallback. Check server logs for upstream failures.

### Build Errors

Expand Down
2 changes: 1 addition & 1 deletion composables/useAccountPositions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const finalizeRefreshCycle = () => {

// Best-effort lookup; does not block capture. Used only to classify Sentry
// events by vault type. The async category fetch works even when RPC is the
// failure mode, since `/api/vault-categories` is subgraph-backed.
// failure mode, since `/api/internal/vault-categories` is subgraph-backed.
const resolveVaultType = async (
vault: string,
): Promise<'evk' | 'earn' | 'securitize' | 'unknown'> => {
Expand Down
2 changes: 1 addition & 1 deletion composables/useBrevis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const inFlightBrevis = createInFlightDedup<number, BrevisCampaignsProxyResponse>

const fetchBrevisCampaignsProxy = (chainId: number): Promise<BrevisCampaignsProxyResponse> =>
inFlightBrevis.run(chainId, () =>
$fetch<BrevisCampaignsProxyResponse>('/api/rewards/brevis', { query: { chainId } }))
$fetch<BrevisCampaignsProxyResponse>('/api/internal/rewards/brevis', { query: { chainId } }))

const ACTION_MAP: Record<string, CampaignAction> = {
EULER_BORROW: CampaignAction.BORROW,
Expand Down
2 changes: 1 addition & 1 deletion composables/useEulerAddresses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export const useEulerAddresses = () => {
error.value = null

try {
const response = await fetch('/api/euler-chains')
const response = await fetch('/api/internal/euler-chains')
if (!response.ok) {
throw new Error(`Failed to fetch Euler config: ${response.statusText}`)
}
Expand Down
14 changes: 7 additions & 7 deletions composables/useEulerLabels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const loadOracleAdapter = async (chainId: number, oracleAddress: string) => {

loadingAdapters.add(key)
try {
const res = await axios.get('/api/oracle-adapter', { params: { chainId, address: checksummed } })
const res = await axios.get('/api/internal/oracle-adapter', { params: { chainId, address: checksummed } })
const meta = normalizeOracleAdapters([res.data])
safeAssign(oracleAdapters, meta)
return oracleAdapters[key]
Expand Down Expand Up @@ -101,7 +101,7 @@ const loadAllOracleAdapters = async (chainId: number): Promise<void> => {

const promise = (async () => {
try {
const res = await axios.get('/api/oracle-adapters', { params: { chainId } })
const res = await axios.get('/api/internal/oracle-adapters', { params: { chainId } })
const meta = normalizeOracleAdapters(res.data)
safeAssign(oracleAdapters, meta)
bulkLoadedAdapterChains.set(chainId, Date.now())
Expand Down Expand Up @@ -167,11 +167,11 @@ export const useEulerLabels = () => {
verifiedVaultAddresses.value = []

const [productRes, entitiesRes, earnRes, pointsRes, assetsRes] = await Promise.allSettled([
axios.get('/api/labels/products.json', { params: { chainId } }),
axios.get('/api/labels/entities.json', { params: { chainId } }),
axios.get('/api/labels/earn-vaults.json', { params: { chainId } }),
axios.get('/api/labels/points.json', { params: { chainId } }),
axios.get('/api/labels/assets.json', { params: { chainId } }),
axios.get('/api/internal/labels/products.json', { params: { chainId } }),
axios.get('/api/internal/labels/entities.json', { params: { chainId } }),
axios.get('/api/internal/labels/earn-vaults.json', { params: { chainId } }),
axios.get('/api/internal/labels/points.json', { params: { chainId } }),
axios.get('/api/internal/labels/assets.json', { params: { chainId } }),
])

if (productRes.status === 'fulfilled') {
Expand Down
2 changes: 1 addition & 1 deletion composables/useFuul.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const inFlightFuul = createInFlightDedup<number, FuulProxyResponse>()

const fetchFuulProxy = (chainId: number): Promise<FuulProxyResponse> =>
inFlightFuul.run(chainId, () =>
$fetch<FuulProxyResponse>('/api/rewards/fuul', { query: { chainId } }))
$fetch<FuulProxyResponse>('/api/internal/rewards/fuul', { query: { chainId } }))

export const useFuul = () => {
const { address: wagmiAddress, chain: wagmiChain } = useAccount()
Expand Down
2 changes: 1 addition & 1 deletion composables/useIntrinsicApy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const useIntrinsicApy = () => {

try {
isLoading.value = true
const data = await $fetch<Record<string, IntrinsicApyInfo>>('/api/intrinsic-apy', {
const data = await $fetch<Record<string, IntrinsicApyInfo>>('/api/internal/intrinsic-apy', {
query: { chainId: chainId.value },
})
intrinsicApyByAddress.value = data ?? {}
Expand Down
4 changes: 2 additions & 2 deletions composables/useMerkl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const merklOpportunityUrl = (
return `https://app.merkl.xyz/opportunities/${encodeURIComponent(chainSlug)}/${type}/${encodeURIComponent(opportunity.identifier)}`
}

// Public campaign data flows through `/api/rewards/merkl`, which warms the
// Public campaign data flows through `/api/internal/rewards/merkl`, which warms the
// three Merkl opportunity types server-side and returns one CDN-cacheable
// GET. User-specific /users/{addr}/rewards stays direct (per-wallet data
// not safe to put on a shared cache).
Expand All @@ -88,7 +88,7 @@ const inFlightMerkl = createInFlightDedup<number, MerklProxyResponse | null>()

const fetchMerklProxy = (chainId: number): Promise<MerklProxyResponse | null> =>
inFlightMerkl.run(chainId, () =>
$fetch<MerklProxyResponse>('/api/rewards/merkl', { query: { chainId } })
$fetch<MerklProxyResponse>('/api/internal/rewards/merkl', { query: { chainId } })
.catch((e) => {
logWarn('merkl/proxy', e)
return null
Expand Down
4 changes: 2 additions & 2 deletions composables/useRpcClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export const useRpcClient = (): {
if (!chainId.value) return ''
if (import.meta.server) {
const requestUrl = useRequestURL()
return `${requestUrl.origin}/api/rpc/${chainId.value}`
return `${requestUrl.origin}/api/internal/rpc/${chainId.value}`
}
return `/api/rpc/${chainId.value}`
return `/api/internal/rpc/${chainId.value}`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

const client = computed(() => {
Expand Down
4 changes: 2 additions & 2 deletions composables/useTenderlySimulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const useTenderlySimulation = () => {
isSimulating.value = true

try {
const response = await $fetch<TenderlySimulateResponse>('/api/tenderly/simulate', {
const response = await $fetch<TenderlySimulateResponse>('/api/internal/tenderly/simulate', {
method: 'POST',
body: params,
})
Expand All @@ -78,7 +78,7 @@ export const useTenderlySimulation = () => {

const fetchEnabled = async (): Promise<boolean> => {
try {
const { enabled } = await $fetch<{ enabled: boolean }>('/api/tenderly/status')
const { enabled } = await $fetch<{ enabled: boolean }>('/api/internal/tenderly/status')
return enabled
}
catch {
Expand Down
2 changes: 1 addition & 1 deletion composables/useTokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const loadTokenList = async (forceRefresh = false) => {
isLoading.value = true
isLoaded.value = false

const res = await axios.get('/api/token-list', { params: { chainId } })
const res = await axios.get('/api/internal/token-list', { params: { chainId } })
if (guard.isStale(gen)) return

const tokens: TokenListEntry[] = res.data?.tokens || []
Expand Down
2 changes: 1 addition & 1 deletion composables/useTosData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function getTosData(): Promise<TosData> {

const { tosUrl } = useDeployConfig()

fetchPromise = fetch('/api/tos')
fetchPromise = fetch('/api/internal/tos')
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch ToS: ${response.status} ${response.statusText}`)
Expand Down
2 changes: 1 addition & 1 deletion composables/useVaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ const hydrateFromServer = async (targetChainId: number, generation: number): Pro
// server-side so this is usually two fast hits. Categorization provides
// the escrow address set previously baked into the snapshot payload.
const [wire, categories] = await Promise.all([
$fetch<SerialisedSnapshot>('/api/vaults', { query: { chainId: targetChainId } }),
$fetch<SerialisedSnapshot>('/api/internal/vaults', { query: { chainId: targetChainId } }),
fetchChainVaultCategories(),
])
if (loadGeneration.value !== generation) return false
Expand Down
22 changes: 22 additions & 0 deletions docs/PUBLIC_API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Public API

These endpoints are the only stable contract this app exposes. Anything under `/api/internal/` is implementation detail and may change without notice — including being renamed, restructured, returning different fields, or being removed entirely.

## Stable endpoints

| Method | Path | Purpose |
|---|---|---|
| `GET` | `/api/public/is-known?chainId=<id>&address=<addr>` | Returns whether the vault address is part of the verified catalogue. |
| `GET` | `/api/public/metadata?chainId=<id>&address=<addr>` | Returns vault metadata for the verified catalogue. |

Both endpoints serve `Access-Control-Allow-Origin: *` so they're callable from any origin.

## Non-public endpoints

Everything else under `/api/` lives at `/api/internal/*` and is reserved for the Euler Lite app itself. Calling them from outside is unsupported:

- Cross-origin browser requests are rejected unless the origin is on the app's allow-list.
- Non-browser requests (curl, server-side fetch) without an `Origin` header are rejected with `403 Forbidden`.
- Every internal response carries `X-API-Stability: internal; may-break-without-notice` for the avoidance of doubt.

The shape, presence, and behaviour of these endpoints may change between releases. If you need data they expose, open a request for an addition to the public surface above rather than relying on the internal namespace.
4 changes: 2 additions & 2 deletions entities/vault/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const fetchChainVaultCategories = async (): Promise<VaultCategories> => {
const existing = chainCategoriesInFlight.get(chainId)
if (existing) return (await existing) ?? emptyCategories()

const promise = $fetch<VaultCategories>('/api/vault-categories', { query: { chainId } })
const promise = $fetch<VaultCategories>('/api/internal/vault-categories', { query: { chainId } })
.then((data) => {
const categories = {
evk: data?.evk ?? [],
Expand Down Expand Up @@ -121,7 +121,7 @@ export const fetchVaultCategory = async (address: string): Promise<VaultCategory
const existing = perAddressInFlight.get(key)
if (existing) return existing

const promise = $fetch<{ category: VaultCategory | null }>('/api/vault-categories', {
const promise = $fetch<{ category: VaultCategory | null }>('/api/internal/vault-categories', {
query: { chainId, address },
})
.then((data) => {
Expand Down
2 changes: 1 addition & 1 deletion entities/vault/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { summarizeViemError } from '~/utils/viem-errors'
* fetches it separately after wallet connect.
*
* Note: escrow categorization used to live here (`escrowAddresses: string[]`)
* but has moved to `/api/vault-categories`. The snapshot no longer carries
* but has moved to `/api/internal/vault-categories`. The snapshot no longer carries
* that field; clients get the escrow set from the categorization endpoint.
*/
export interface ChainVaultsSnapshot {
Expand Down
2 changes: 1 addition & 1 deletion plugins/00.wagmi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default defineNuxtPlugin((nuxtApp) => {
const publicHttp = network.rpcUrls?.default?.http ?? []
transports[chainId] = fallback(
[
http(`/api/rpc/${chainId}`, batchConfig),
http(`/api/internal/rpc/${chainId}`, batchConfig),
// Public fallback gets the same batch config so a proxy outage doesn't
// explode into 50-100× more individual requests to the public endpoint.
...publicHttp.map(url => http(url, batchConfig)),
Expand Down
2 changes: 1 addition & 1 deletion sentry.client.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ if (sentryDsn) {
Sentry.init({
dsn: sentryDsn,
environment: appUrl?.includes('stage') ? 'staging' : 'production',
tunnel: '/api/sentry-tunnel',
tunnel: '/api/internal/sentry-tunnel',
tracesSampleRate: 0.2,
replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0.05,
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
6 changes: 3 additions & 3 deletions server/middleware/body-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ const SENTRY_LIMIT = 5 * 1024 * 1024
const DEFAULT_LIMIT = 1 * 1024 * 1024

function getLimit(pathname: string): number {
if (pathname.startsWith('/api/tenderly/')) return TENDERLY_LIMIT
if (pathname.startsWith('/api/rpc/')) return RPC_LIMIT
if (pathname === '/api/sentry-tunnel') return SENTRY_LIMIT
if (pathname.startsWith('/api/internal/tenderly/')) return TENDERLY_LIMIT
if (pathname.startsWith('/api/internal/rpc/')) return RPC_LIMIT
if (pathname === '/api/internal/sentry-tunnel') return SENTRY_LIMIT
return DEFAULT_LIMIT
}

Expand Down
17 changes: 17 additions & 0 deletions server/middleware/cors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createError, getRequestURL, setResponseHeader, sendNoContent } from 'h3'
import { logger } from '~/server/utils/logger'
import { isInternalRequest } from '~/server/utils/internal-headers'

function parseAllowedOrigins(): Set<string> {
// CORS_ALLOWED_ORIGINS is the dedicated CORS var (comma-separated).
Expand Down Expand Up @@ -92,6 +93,11 @@ export default defineEventHandler((event) => {
return
}

// Everything else under /api/* is implementation detail. Flag it on the
// response so anyone watching the network tab (their own DevTools, their
// logging pipeline) sees the warning even if they ignore the URL prefix.
setResponseHeader(event, 'X-API-Stability', 'internal; may-break-without-notice')

const origin = event.node.req.headers.origin

if (origin && allowedOrigins.has(origin)) {
Expand All @@ -103,6 +109,17 @@ export default defineEventHandler((event) => {
}
throw createError({ statusCode: 403, statusMessage: 'Origin not allowed' })
}
else if (!origin && !isInternalRequest(event) && process.env.DOPPLER_ENVIRONMENT !== 'dev') {
// No Origin header in prod means a non-browser caller (curl, server-side
// fetch, etc.) hitting an internal endpoint. Same-app server-to-server
// traffic bypasses via the loopback `cf-connecting-ip` sentinel set by
// INTERNAL_FETCH_HEADERS — anyone else gets the door closed politely.
logger.warn({ ctx: 'cors', path: url.pathname }, 'rejected internal endpoint call without Origin')
throw createError({
statusCode: 403,
statusMessage: 'Forbidden — /api/internal/* is not a public contract. See docs/PUBLIC_API.md.',
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

setResponseHeader(event, 'Access-Control-Allow-Methods', 'POST, OPTIONS')
setResponseHeader(event, 'Access-Control-Allow-Headers', 'Content-Type')
Expand Down
6 changes: 3 additions & 3 deletions server/plugins/warm-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
* (one of its sources). Merkl's ERC20LOGPROCESSOR refresh also calls
* `getVaultCategories(chainId)` to filter by the chain earn set.
*/
import { LABEL_FILES, refreshLabelFile } from '../api/labels/[file].get'
import { refreshEulerChains } from '../api/euler-chains.get'
import { refreshTokenList } from '../api/token-list.get'
import { LABEL_FILES, refreshLabelFile } from '../api/internal/labels/[file].get'
import { refreshEulerChains } from '../api/internal/euler-chains.get'
import { refreshTokenList } from '../api/internal/token-list.get'
import { getEnabledChainIds } from '~/utils/chain-env'
import { parseDeprecatedChains } from '~/utils/parseDeprecatedChains'
import { reportStatus } from '../utils/log'
Expand Down
2 changes: 1 addition & 1 deletion server/utils/escrow-perspective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface EulerChainConfig {
}

async function fetchEulerChains(): Promise<EulerChainConfig[]> {
const data = await $fetch<unknown>('/api/euler-chains', { headers: INTERNAL_FETCH_HEADERS })
const data = await $fetch<unknown>('/api/internal/euler-chains', { headers: INTERNAL_FETCH_HEADERS })
return Array.isArray(data) ? data as EulerChainConfig[] : []
}

Expand Down
2 changes: 1 addition & 1 deletion server/utils/labels-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function fetchLabels<T>(
chainId: number,
file: 'products.json' | 'entities.json' | 'earn-vaults.json',
): Promise<T> {
return await $fetch<T>(`/api/labels/${file}`, {
return await $fetch<T>(`/api/internal/labels/${file}`, {
query: { chainId },
headers: INTERNAL_FETCH_HEADERS,
})
Expand Down
2 changes: 1 addition & 1 deletion server/utils/labels-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ function isHttpUrl(value: string): boolean {
}

async function fetchTokenList(chainId: number): Promise<TokenListEntry[]> {
const data = await $fetch<TokenListResponse>('/api/token-list', {
const data = await $fetch<TokenListResponse>('/api/internal/token-list', {
query: { chainId },
headers: INTERNAL_FETCH_HEADERS,
})
Expand Down
2 changes: 1 addition & 1 deletion server/utils/vault-categories-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ interface EulerChainsResponse {

const getChainFactoryAddresses = async (chainId: number): Promise<ChainFactoryAddresses> => {
try {
const chains = await $fetch<EulerChainsResponse[]>('/api/euler-chains', { headers: INTERNAL_FETCH_HEADERS })
const chains = await $fetch<EulerChainsResponse[]>('/api/internal/euler-chains', { headers: INTERNAL_FETCH_HEADERS })
const entry = chains.find(c => c.chainId === chainId)
const core = entry?.addresses?.coreAddrs
const periphery = entry?.addresses?.peripheryAddrs
Expand Down
6 changes: 3 additions & 3 deletions server/utils/vaults-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ interface EarnVaultEntry {
}

const getChainConfig = async (chainId: number): Promise<EulerChainEntry | undefined> => {
const chains = await $fetch<EulerChainEntry[]>('/api/euler-chains', { headers: INTERNAL_FETCH_HEADERS })
const chains = await $fetch<EulerChainEntry[]>('/api/internal/euler-chains', { headers: INTERNAL_FETCH_HEADERS })
return chains.find(c => c.chainId === chainId)
}

Expand All @@ -73,11 +73,11 @@ const getChainConfig = async (chainId: number): Promise<EulerChainEntry | undefi
*/
const getLabels = async (chainId: number) => {
const [products, earn] = await Promise.all([
$fetch<Record<string, EulerLabelProduct>>('/api/labels/products.json', {
$fetch<Record<string, EulerLabelProduct>>('/api/internal/labels/products.json', {
query: { chainId },
headers: INTERNAL_FETCH_HEADERS,
}).catch(() => ({} as Record<string, EulerLabelProduct>)),
$fetch<Array<string | EarnVaultEntry>>('/api/labels/earn-vaults.json', {
$fetch<Array<string | EarnVaultEntry>>('/api/internal/labels/earn-vaults.json', {
query: { chainId },
headers: INTERNAL_FETCH_HEADERS,
}).catch(() => [] as Array<string | EarnVaultEntry>),
Expand Down
2 changes: 1 addition & 1 deletion services/trm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export async function screenAddress(
if (!address) return false

try {
const resp = await fetch('/api/screen-address', {
const resp = await fetch('/api/internal/screen-address', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, vpnIsUsed }),
Expand Down
2 changes: 1 addition & 1 deletion tests/server/internal-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* If this contract breaks — the sentinel stops being set, the helper
* stops recognising it, or a middleware forgets to consult the helper —
* every internal API→API call 502s/451s in prod. One such regression
* already shipped once (internal `/api/vaults` → `/api/euler-chains`
* already shipped once (internal `/api/internal/vaults` → `/api/internal/euler-chains`
* 451'd by geo-gate). Lock it down.
*/
import { describe, it, expect } from 'vitest'
Expand Down
2 changes: 1 addition & 1 deletion tests/server/labels-validate-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* corresponding defense (see git blame on server/api/labels/[file].get.ts).
*/
import { describe, it, expect } from 'vitest'
import { validateNode } from '~/server/api/labels/[file].get'
import { validateNode } from '~/server/api/internal/labels/[file].get'

describe('validateNode — size caps', () => {
it('rejects strings longer than 16 KiB (client-side DoS guard)', () => {
Expand Down
Loading