Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d8ed424
chore(deps): bump svgo
dependabot[bot] Sep 9, 2026
7284560
fix: exclude external vaults from migration snapshots
dglowinski Sep 9, 2026
ab3a994
refactor: provider-neutral internal fetch auth with origin-secret marker
kasperpawlowski Aug 21, 2026
b2fde88
feat: normalized edge context with origin auth and production boot guard
kasperpawlowski Aug 21, 2026
19e0177
refactor: consume the edge context in rate-limit, geo-gate, cors, and…
kasperpawlowski Aug 21, 2026
13011db
feat: skip the client VPN probe on edges without VPN evidence
kasperpawlowski Aug 21, 2026
3927cb4
docs: provider-neutral edge model in env, architecture, and geo docs
kasperpawlowski Aug 21, 2026
14c3eb1
fix: never honor the internal sentinel under presets whose edge forwa…
kasperpawlowski Aug 21, 2026
323eaf9
fix: probe /healthz for container liveness instead of a gated interna…
kasperpawlowski Aug 21, 2026
4a6d1f4
fix: replace the loopback internal sentinel with an unforgeable per-p…
kasperpawlowski Aug 21, 2026
84a8a1a
fix: recorder V3 preflight authenticates as a first-party caller, not…
kasperpawlowski Aug 21, 2026
182086f
fix: keep client positive VPN signal alongside edge-derived evidence
kasperpawlowski Sep 9, 2026
9fb52a3
fix: reject DEV_GEO_COUNTRY in production and document none/google pr…
kasperpawlowski Sep 9, 2026
e0ee9a4
refactor: type window.__APP_CONFIG__ with the shared EnvConfig shape
kasperpawlowski Sep 9, 2026
e441cf0
Merge pull request #856 from euler-xyz/fix/morpho-migration-review-sn…
Seranged Sep 10, 2026
732c9aa
Merge pull request #825 from euler-xyz/feat/edge-provider-abstraction
Seranged Sep 10, 2026
a371054
Merge pull request #855 from euler-xyz/dependabot/npm_and_yarn/multi-…
Seranged Sep 10, 2026
cd56ec3
chore(deps): bump js-yaml from 4.3.1 to 4.3.2
dependabot[bot] Sep 10, 2026
2ee0fa6
Merge pull request #860 from euler-xyz/dependabot/npm_and_yarn/js-yam…
Seranged Sep 10, 2026
40834da
fix: keep VPN usage as audit metadata
Seranged Sep 10, 2026
67d01d4
Merge pull request #861 from euler-xyz/fix/vpn-telemetry-only
Seranged Sep 10, 2026
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
32 changes: 30 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,36 @@
# origin rejection, and HSTS. Injected automatically by Doppler at runtime.
DOPPLER_ENVIRONMENT=dev

# Fallback country when cf-ipcountry is absent (local dev, PR previews, any env without Cloudflare).
# Bypasses fail-closed geo-gate — do not set in production behind Cloudflare.
# Fronting edge provider preset (cloudflare | google | cloudfront | none).
# Selects which trusted request headers carry the client IP, country, and
# VPN evidence (the mapping lives in utils/edge-presets.ts). Default: none —
# no edge-derived trust, geo-blocking off, rate limiting on best-effort
# identity. Fork- and preview-friendly, but REQUIRED in production
# (DOPPLER_ENVIRONMENT=prd): the server refuses to boot when unset there.
# Setting it to "none" in production is an explicit opt-out with no trusted
# identity (rate-limit keys are forgeable) and is logged as a warning at boot.
# The google preset additionally needs the LB configured to stamp
# x-client-geo: {client_region} as a custom request header.
EDGE_PROVIDER=

# Origin-auth shared secret (server-side only). When set, every request
# must carry a matching x-edge-origin-auth header — stamped by the edge
# (e.g. a request-header transform rule) — or the edge-derived inputs are
# treated as absent and the fail-closed paths apply. This replaces the
# "origin is only reachable through the edge" topology assumption with a
# check the app enforces itself. Optional for the cloudflare/none presets;
# REQUIRED for google/cloudfront (their edges forward client headers
# untouched, so without origin auth their trusted inputs would be forgeable
# — the server refuses to boot without it). Internal server-to-server
# fetches authenticate with a random per-process marker when this is unset,
# so no preset depends on the secret for internal traffic. Configure the
# edge to stamp the header BEFORE setting this, and to strip
# client-supplied x-edge-internal.
EDGE_ORIGIN_SECRET=

# Fallback country when the edge provides none (local dev, PR previews, any
# env without a geo-capable edge). Bypasses the fail-closed geo-gate, so
# production (DOPPLER_ENVIRONMENT=prd) refuses to boot when it is set.
DEV_GEO_COUNTRY=GB

# Reown (AppKit) configuration
Expand Down
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,11 @@ Euler Lite is the only service. Standard commands live in `README.md` ("Availabl
Note: `internal/screen-address` is additionally consumed cross-origin by first-party
`*.euler.finance` SPAs via a path-scoped CORS exception in `cors.ts` — keep its contract
backward-compatible.
- **Server middleware:** `geo-gate.ts` (451 for sanctioned countries via Cloudflare `CF-IPCountry`;
set `DEV_GEO_COUNTRY` locally since there's no CF header), `cors.ts`, `security-headers.ts`,
`body-limit.ts`, `ensure-vault.ts`.
- **Server middleware:** `geo-gate.ts` (451 for sanctioned countries via the country from
`getEdgeContext` — trusted-header mapping per `EDGE_PROVIDER` preset in `utils/edge-presets.ts`;
set `DEV_GEO_COUNTRY` locally since there's no edge header), `cors.ts`, `security-headers.ts`,
`body-limit.ts`, `ensure-vault.ts`. Middleware and routes stay vendor-neutral: edge header
names live only in the presets file, consumed through `server/utils/edge.ts`.
- **Server plugins (load order matters):** `app-config.ts` / `chain-config.ts` inject the `window`
config; `csp.ts` (nonce-based CSP) must run after them; `warm-cache.ts` warms labels/token-list/
vault caches in the background. Caching internals are in `docs/server-side-caching.md`.
Expand Down
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@ COPY --from=doppler /usr/local/bin/doppler ./doppler

EXPOSE ${APP_PORT}

# Liveness probe: /healthz lives outside /api/ so it is exempt from the
# geo-gate, rate limiting, and internal-request authentication — the probe
# must not depend on edge configuration (EDGE_PROVIDER / EDGE_ORIGIN_SECRET)
# and must never carry the origin secret in its arguments.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["/nodejs/bin/node", "-e", "fetch('http://localhost:'+process.env.PORT+'/api/internal/tenderly/status',{headers:{'cf-connecting-ip':'127.0.0.1'}}).then(r=>{if(!r.ok)throw r.status}).catch(()=>process.exit(1))"]
CMD ["/nodejs/bin/node", "-e", "fetch('http://localhost:'+process.env.PORT+'/healthz').then(r=>{if(!r.ok)throw r.status}).catch(()=>process.exit(1))"]

# Doppler injects all secrets at runtime via DOPPLER_TOKEN, DOPPLER_PROJECT, DOPPLER_CONFIG env vars.
# server/plugins/chain-config.ts scans env vars and injects chain config via render:html hook.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Euler Lite uses the [Euler V2 SDK](https://github.com/euler-xyz/euler-sdks) for
| `CORS_ALLOWED_ORIGINS` | Comma-separated allowlist for `/api/*`; falls back to `NUXT_PUBLIC_APP_URL`. |
| `FIRST_PARTY_COOKIE_SECRET` | Optional server-only secret that keeps the internal API marker cookie stable across replicas and deploys. Defaults to a value derived from `NUXT_PUBLIC_APP_URL` or `RAILWAY_PUBLIC_DOMAIN`. |
| `CSP_EXTRA_CONNECT_SRC` | Extra `connect-src` origins for development or staging endpoints. |
| `DEV_GEO_COUNTRY` | Local/preview country fallback when Cloudflare geo headers are absent. Do not set in production behind Cloudflare. |
| `DEV_GEO_COUNTRY` | Local/preview country fallback when the edge provides no country. Production (`DOPPLER_ENVIRONMENT=prd`) refuses to boot with it set. |
| `ADDRESS_SCREENING_URI` / `ADDRESS_SCREENING_API_KEY` | Server-side data-v3 compliance endpoint + restricted API key, proxied by `/api/internal/screen-address` (also serves first-party `*.euler.finance` SPAs). Both unset ⇒ screening disabled (all addresses pass) — except in production (`DOPPLER_ENVIRONMENT=prd`), where missing configuration fails closed; only one set ⇒ fails closed everywhere. URI must be https (localhost http allowed for dev). |
| `MERKL_API_KEY` | Optional server-side Merkl key. The Merkl API works anonymously (10 req/sec shared across all users via `/api/internal/proxy/merkl`); set this to send `X-API-Key` upstream for a higher quota. Server-only — never exposed to the browser. |
| `TENDERLY_ACCESS_KEY`, `TENDERLY_ACCOUNT_SLUG`, `TENDERLY_PROJECT_SLUG` | Optional Tenderly simulation configuration. |
Expand Down
10 changes: 1 addition & 9 deletions composables/useAddressScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,7 @@ export const useAddressScreen = () => {
const vpnIsUsed = await detectVpn()
if (gen !== screeningGeneration) return false

// A positive local signal is independently blocking. A clean or failed
// remote address-screen response must never erase it.
if (vpnIsUsed) {
await disconnect()
if (gen !== screeningGeneration) return false
showBlockedModal(address)
return true
}

// VPN usage is audit metadata; only the address-screening verdict gates access.
const isRestricted = await screenAddress(address, vpnIsUsed)
if (gen !== screeningGeneration) return false

Expand Down
25 changes: 20 additions & 5 deletions composables/useEnvConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import {
EMPTY_ANNOUNCEMENT_CONFIG,
type AnnouncementConfig,
} from '~/utils/announcement-config'
import { edgeProvidesVpnEvidence, parseEdgeProvider } from '~/utils/edge-presets'

interface EnvConfig {
export interface EnvConfig {
appTitle: string
appDescription: string
logoUrl: string
Expand All @@ -42,6 +43,16 @@ interface EnvConfig {
swapApiUrl: string
eulerInterfacesBranch: string
announcement: AnnouncementConfig
/** Whether the deployment's edge provider measures VPN usage. Drives the
* client VPN probe in services/vpn.ts — false skips it entirely. */
vpnDetection: boolean
}

declare global {
interface Window {
/** Server-injected runtime config (server/plugins/app-config.ts). */
__APP_CONFIG__?: EnvConfig
}
}

const DEFAULTS: EnvConfig = {
Expand All @@ -58,6 +69,7 @@ const DEFAULTS: EnvConfig = {
swapApiUrl: '',
eulerInterfacesBranch: 'master',
announcement: EMPTY_ANNOUNCEMENT_CONFIG,
vpnDetection: false,
}

let cached: EnvConfig | null = null
Expand Down Expand Up @@ -94,6 +106,7 @@ function scanEnv(): EnvConfig {
items: env('CONFIG_ANNOUNCEMENT_ITEMS', 'NUXT_PUBLIC_CONFIG_ANNOUNCEMENT_ITEMS'),
url: env('CONFIG_ANNOUNCEMENT_URL', 'NUXT_PUBLIC_CONFIG_ANNOUNCEMENT_URL'),
}),
vpnDetection: edgeProvidesVpnEvidence(parseEdgeProvider(process.env.EDGE_PROVIDER)),
}
}

Expand Down Expand Up @@ -130,6 +143,10 @@ function fromRuntimeConfig(): EnvConfig {
items: rc.configAnnouncementItems,
url: rc.configAnnouncementUrl,
}),
// Static/CDN deployments carry no edge preset information — skip the
// VPN probe (the server derives the authoritative verdict from edge
// request headers regardless).
vpnDetection: false,
}
}

Expand All @@ -139,10 +156,8 @@ export const useEnvConfig = (): EnvConfig => {
if (import.meta.server) {
cached = scanEnv()
}
/* eslint-disable @typescript-eslint/no-explicit-any -- server-injected window global */
else if (typeof window !== 'undefined' && (window as any).__APP_CONFIG__) {
cached = (window as any).__APP_CONFIG__
/* eslint-enable @typescript-eslint/no-explicit-any */
else if (typeof window !== 'undefined' && window.__APP_CONFIG__) {
cached = window.__APP_CONFIG__
}
else {
cached = fromRuntimeConfig()
Expand Down
Loading
Loading