Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
9386bb4
feat(theme): dark-only token system and Geist type foundation
Coder-soft Oct 9, 2026
0a58f37
fix(theme): restore original brand purple and document scrolling
Coder-soft Oct 9, 2026
fe67864
feat(ui): redesign home components (batch 2)
Coder-soft Oct 9, 2026
0c5311e
feat(ui): make buttons full pills
Coder-soft Oct 9, 2026
64bfa2c
feat(ui): redesign resources hub (batch 3)
Coder-soft Oct 9, 2026
c6bda25
feat(ui): Montserrat Bold headings
Coder-soft Oct 9, 2026
7acfb5f
feat(ui): sweep remaining resource components (batch 4)
Coder-soft Oct 9, 2026
68760d0
feat(ui): sweep remaining pages and components (batch 5)
Coder-soft Oct 9, 2026
11f2125
refactor(theme): drop dead pixel/cow CSS and unused fonts
Coder-soft Oct 9, 2026
402f87f
fix(ui): QA pass — dialog titles and mangled hover utilities
Coder-soft Oct 9, 2026
89164c8
fix(favorites): key card favourites by resource URL
Coder-soft Oct 9, 2026
34f24e3
feat(ui): pop transition between donate and back-to-top buttons
Coder-soft Oct 9, 2026
43569e6
feat(ui): rework Background Gen hover preview
Coder-soft Oct 9, 2026
ce46c58
fix(ui): resource hub polish
Coder-soft Oct 9, 2026
37b732c
fix(mcicons): make the category browser scrollable
Coder-soft Oct 9, 2026
41b4efb
fix(ui): use overflow-x: clip on the root so the page scrolls
Coder-soft Oct 9, 2026
64ad760
fix(mcicons): use a plain scroll container in the category browsers
Coder-soft Oct 10, 2026
47afd32
fix(ui): stop SelectContent from locking page scroll
Coder-soft Oct 10, 2026
2998e79
feat(ui): pixel browser icons, unbold headers, compact audio player
Coder-soft Oct 10, 2026
a7129bc
fix(ui): render browser header labels in body font (not headings)
Coder-soft Oct 10, 2026
4416df6
feat(ui): restyle the card category label
Coder-soft Oct 10, 2026
20d8b4c
chore(ui): remove 'Powered by' caption from the resources hub
Coder-soft Oct 10, 2026
eca87ac
fix(ui): hide the waveform cursor bar in the audio player
Coder-soft Oct 10, 2026
c626af5
fix(ui): music packs browser layout
Coder-soft Oct 10, 2026
3c08b9f
style(ui): remove translucent surfaces
Coder-soft Oct 10, 2026
a4e62ad
feat(ui): show YouTube icon + video title on music pack links
Coder-soft Oct 10, 2026
d7654f0
feat(ui): load music moods from the assets API
Coder-soft Oct 10, 2026
66418c6
fix(ui): restore the waveform playhead
Coder-soft Oct 10, 2026
4cd648c
fix(ui): match the waveform loading/error overlay background to the w…
Coder-soft Oct 10, 2026
38788da
fix(ui): font previews, dialog category label, contact redesign
Coder-soft Oct 10, 2026
c559dd2
fix(ui): only one audio preview can play at a time
Coder-soft Oct 10, 2026
081456a
feat(ui): chamfered "render panel" corner language
Coder-soft Oct 11, 2026
2106b28
wip: mcicons animation, admin tabs, analytics, font option
Coder-soft Oct 13, 2026
8f1ebeb
Merge feat/admin-panel-redesign into redesign (admin tabs, mcicons an…
Coder-soft Oct 13, 2026
4a6a5de
feat(partners): add invite-only partner program with OAuth consent
Coder-soft Oct 15, 2026
dc040e7
feat(ui): auto-hide the navbar on scroll down
Coder-soft Oct 15, 2026
497f1c5
fix(ui): lowercase the mcicons breadcrumb category
Coder-soft Oct 15, 2026
4a7c0a0
feat(ui): rebuild background generator as a two-pane editor
Coder-soft Oct 15, 2026
3c0c5e8
fix(ui): zero every remaining rounded corner
Coder-soft Oct 15, 2026
08b9818
Merge remote-tracking branch 'origin/redesign/app' into redesign/app
Coder-soft Oct 15, 2026
5005c1b
style(partners): drop the pill corners to match the sharp-corner lang…
Coder-soft Oct 15, 2026
d157aca
feat(partners): onboarding, connected apps, cross-origin assets gateway
Coder-soft Oct 15, 2026
d4ea326
fix(partners): address code review
Coder-soft Oct 15, 2026
84a5184
docs(partners): add /partners/docs guide and llms.txt
Coder-soft Oct 16, 2026
184a09f
docs(partners): syntax-highlight the guide code blocks
Coder-soft Oct 16, 2026
2d41baf
fix(account-menu): drop Edit Profile, fix My Favorites link
Coder-soft Oct 16, 2026
5c6f131
feat(router): multi-origin pools with per-origin circuit breaking
Coder-soft Oct 16, 2026
f1290c6
feat(chibi): add the Minecraft chibi maker
Coder-soft Oct 18, 2026
fc615b9
feat(status): add the public status page
Coder-soft Oct 18, 2026
4b9a937
refactor(player-renderer): simplify the player fetch and easter eggs
Coder-soft Oct 18, 2026
939075e
feat(nav): route the chibi maker and status pages
Coder-soft Oct 18, 2026
bea1b43
fix(generators): use a renderdragon placeholder avatar
Coder-soft Oct 18, 2026
9819711
copy: tighten page copy and meta descriptions
Coder-soft Oct 18, 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
166 changes: 156 additions & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,167 @@
# AGENTS.md

## Repository layout
## Which checkout to work in

This checkout is the `Coder-soft/renderdragon.org` fork. The canonical upstream
for this project is **`creatorcluster/renderdragon.org`** (base branch `main`),
which itself forks `Yxmura/renderdragon`.
- **Canonical:** the worktree **`/Users/techspot/renderdragon-redesign`** (branch `redesign/app`)
of the main repo `/Users/techspot/renderdragon.org`. Do your work here.
- `/Users/techspot/reddragon/renderdragon.org` is a **separate, stale clone** (behind by many
commits). Do not work in it; it caused a lot of "wrong code / wrong port" confusion.
- `/Users/techspot/renderdragon.org` (main checkout) is a worktree on `feat/admin-panel-redesign`.
- `origin` = `Coder-soft/renderdragon.org` (fork), `upstream` = `creatorcluster/renderdragon.org`.

## Pull requests
## Repository layout / pull requests

Open every pull request against upstream **`creatorcluster/renderdragon.org` `main`**,
not against the `Coder-soft` fork. Push the branch to `origin` (the fork) and open a
cross-repository PR:
This checkout is the `Coder-soft/renderdragon.org` fork. The canonical upstream for this project is
**`creatorcluster/renderdragon.org`** (base branch `main`), which itself forks `Yxmura/renderdragon`.

Open every pull request against upstream **`creatorcluster/renderdragon.org` `main`**, not against the
`Coder-soft` fork. Push the branch to `origin` (the fork) and open a cross-repository PR:

```sh
git push -u origin <branch>
gh pr create --repo creatorcluster/renderdragon.org --base main --head Coder-soft:<branch>
```

GitHub's native linked-branch feature (`gh issue develop`) does not work in the fork,
so reference the relevant fork issue by URL in the PR body instead.
GitHub's native linked-branch feature (`gh issue develop`) does not work in the fork, so reference the
relevant fork issue by URL in the PR body instead. There is an existing open redesign+partners PR
(`Coder-soft:redesign/app` → `creatorcluster:main`); pushing to that branch updates it.

## Dev environment

- `pnpm dev` runs **Vite on :8080** plus an **Express server on :3000** (`server.js`, which adapts the
`api/*.js` Vercel handlers and `/api/uploadthing`). Run **one** dev server only.
- **Dev assets bypass the gateway**: `vite.config.ts` proxies `/api/assets-gw/*` straight to the public
upstreams. Only production goes through `gateway.codersoft.xyz`.
- Commands: `pnpm lint`, `pnpm exec tsc -b`, `pnpm exec vite build`.
- `tsc` has **pre-existing errors** in `ProfileEditor`/`useCreatorPacks`/`useProfile`/`migrateResources`/
`AdminResourcesManager`/`chart`/`Analytics` — ignore them; do not "fix" unrelated ones.
- If deps look stale/wrong (missing module, version mismatch): `pnpm install --frozen-lockfile --config.confirmModulesPurge=false`.
- `/account` = Connected apps; `/partners/dashboard` = partner dashboard; `/oauth/consent` = consent screen.

## Infrastructure map

- **Supabase** project `bmywdrwjdqmrkafhiuwn`. Schema/RLS in `supabase/migrations/*.sql` (all applied).
Edge Function `partner-keys` (key provisioning). Auth config via Management API.
- **Cloudflare account** "Powernplant101@gmail.com's Account" (id `c6bc31479acbdc8f997bcfa3cacb42b4`).
Zone **`codersoft.xyz` is in this account; `renderdragon.org` is in a DIFFERENT Cloudflare account** —
that's why the browser gateway lives on `gateway.codersoft.xyz` instead of a `renderdragon.org` route.
- **Workers** (sources in `workers/`):
- `codersoft-api-router` → **`api.codersoft.xyz`** (Durable Object `AuthStore`). Verified request auth,
scopes/rate limits, ordered origin pools with per-origin circuit breaking (`RESOURCES_ORIGINS` /
`ASSETS_ORIGINS`), `__version`, `__admin/*`.
- `codersoft-assets-gateway` → **`gateway.codersoft.xyz`** (holds `GATEWAY_SECRET`, minted browser
sessions, CORS + `SameSite=None` cookies, proxies to the router).
- **e3** (ssh alias `e3`, `home@192.168.100.30`, Debian): systemd service **`hamburger-api`** on
`127.0.0.1:8788`, source `/home/home/assets-api/server.mjs`, env `/etc/hamburger-api.env`. It builds
the asset catalog from a local clone `/home/home/assets-renderdragon` (of
`Renderdragonorg/resources_renderdragon`) and serves the **files from disk** (not GitHub). Exposed via
the Cloudflare Tunnel as **`hamburger.codersoft.xyz`**.
- **Data flow:** browser → `gateway.codersoft.xyz` (session, signs) → `api.codersoft.xyz` (verifies) →
e3 `hamburger.codersoft.xyz` (resources catalog + files) / `assets-api-worker` (uploads) → on e3 down,
fall back to `hamburger-api`/`assets-api-worker`.

## Auth model (why "network manipulation" can't bypass)

- The router requires **HMAC-SHA256 signed requests** (`X-Auth-Client/Timestamp/Nonce/Signature`) for
every endpoint except `OPTIONS`, `/__router/health`, `/__version`, `/__admin/*`. Canonical string:
`v1\nMETHOD\nPATH?QUERY\nCLIENT_ID\nTIMESTAMP\nNONCE\nSHA256_HEX(BODY)`. ±300s window; nonce replay
cache (Durable Object) for writes.
- **Partner keys** carry scopes (e.g. `assets:read`) and rate limits; read-only + path-limited.
- **e3** verifies the router's `X-E3-*` signature with `E3_SHARED_SECRET` (same canonical string, client
id `api-router`).
- **Supabase OAuth 2.1 server** is enabled (`site_url=https://renderdragon.org`,
`oauth_server_authorization_path=/oauth/consent`, dynamic registration off). Consent notes below.

## Secrets (NAMES ONLY — never print, echo, or commit values)

- Router Worker: `ADMIN_KEY`, `E3_SHARED_SECRET`.
- Gateway Worker: `GATEWAY_SECRET`, `TURNSTILE_SECRET`; var `SUPABASE_ANON_KEY` (publishable, not secret).
- Edge Function `partner-keys`: `ROUTER_ADMIN_KEY` (= the router's `ADMIN_KEY`).
- e3: `/etc/hamburger-api.env` → `E3_SHARED_SECRET`, `WEBHOOK_SECRET`.
- GitHub webhook on `Renderdragonorg/resources_renderdragon` → `WEBHOOK_SECRET`.
- Frontend `.env`: `VITE_SUPABASE_URL`, `VITE_SUPABASE_PUBLISHABLE_KEY`; production also needs
`VITE_TURNSTILE_SITE_KEY`.
- Tooling creds: `~/.config/sbase/.env` (`SBASE_ACCESS_TOKEN`), `~/.wrangler/config/default.toml`
(Cloudflare OAuth token). e3 root password is documented in the global `e3-server` skill.

## Partner program model

- Invite-only. **1 organization = 1 OAuth app** (`partner_apps.org_id` unique).
- Permissions live in **`src/lib/partners/permissions.ts`** (currently `profile`, `email`,
`favourites:read`) — the single source of truth driving the consent screen, the dashboard, and API
scopes. Add/remove there and it propagates everywhere.
- `partner_consents` stores the approved profile snapshot; the user-facing **Connected apps** section on
`/account` reads the `get_my_connected_apps()` RPC and revokes via `partner_consents.revoked_at`.
- The consent page only records a consent when the OAuth client maps to a
`partner_apps.supabase_client_id` (via the `partner_app_public` RPC). A raw OAuth client that isn't a
provisioned partner app won't appear in Connected apps.
- Register partner/clients: `POST /auth/v1/admin/oauth/clients` (service role). Delete demo clients when
done.

## Operational pitfalls (learned the hard way)

- **Cloudflare bot protection** returns `1010` for clients with no/blocked `User-Agent`. Real clients send
one; python-urllib is blocked.
- **wrangler OAuth token expires**; running any `wrangler` command refreshes it. The token has **no DNS
scope** — create DNS records with `cloudflared tunnel route dns <tunnel-id> <hostname>` on e3 (uses
`~/.cloudflared/cert.pem`), not the CF API.
- **sbase / ssh sudo:** never feed the root password and a `bash -s` heredoc down the same stdin (sudo eats
it). Stage the script to a file and run `echo root | sudo -S -p "" bash /tmp/x.sh`.
- **React StrictMode** double-invokes effects in dev — a one-time request (OAuth authorization) gets
consumed twice and 400s. Guard with a ref (`startedRef` in `OAuthConsent.tsx`).
- **Supabase OAuth consent endpoints** (`/oauth/authorizations/*`) require the **legacy anon JWT** as the
`apikey` header (the `sb_publishable_…` key is rejected there). Confidential clients authenticate at
`/oauth/token` with **`client_secret_basic`**. Consent POST body is `{ "action": "approve"|"deny" }`.
- **e3 powers off nightly 03:00–05:00 PKT** (RTC wake 05:00), so the router falls back to the Cloudflare
workers. While e3's catalog is empty (e.g. mid-clone) it returns `503` so the router also falls back.
- **Avatars:** third-party hosts get hidden by ad/tracker blockers — served same-origin via `/api/avatar`
(`api/avatar.js` + the `server.js` route), and the client rewrites URLs with `toDisplayAvatarUrl`.
- **Assets in dev bypass the gateway**; production uses `gateway.codersoft.xyz`.
- **This Mac's tooling:** `jq` is NOT installed (parse with `python3`); `python3` lacks a CA bundle (use an
unverified `ssl` context, or just `curl`); in zsh never `echo "$JSON"` (interprets escapes) — use
`printf '%s'`.
- **Design language:** the redesign `index.css` zeroes every radius
(`[class*="rounded-"]:not(.rounded-full){border-radius:0!important}`) and uses `chamfer`; avoid `rounded-*`
corners. Colors/type come from the token system (dark, primary purple, Geist/Montserrat).
- **Multi-checkout port confusion** is real — keep a single dev server and kill strays on :8080–:8084.

## Deploy & verify

```sh
# Workers
cd workers/<api-router|assets-gateway> && pnpm install --ignore-workspace && pnpm exec wrangler deploy

# SQL migration
sbase apply-migration --project-id bmywdrwjdqmrkafhiuwn --name <name> --file <path.sql>

# Edge Function
sbase deploy-edge-function --project-id bmywdrwjdqmrkafhiuwn --name partner-keys \
--file index.ts=supabase/functions/partner-keys/index.ts --entrypoint-path index.ts

# e3 assets service (after editing /home/home/assets-api/server.mjs or credits.json)
ssh e3 'echo root | sudo -S -p "" systemctl restart hamburger-api'

# Health
curl https://api.codersoft.xyz/__router/health
curl https://gateway.codersoft.xyz/__version
curl https://hamburger.codersoft.xyz/__health
```

- **Auto-update:** pushing to `Renderdragonorg/resources_renderdragon` fires its webhook →
`https://hamburger.codersoft.xyz/__update` (HMAC-verified) → `git pull` + rebuild the catalog.
- **Signed-request check:** mint a key via the `partner-keys` Edge Function, then sign
`v1\nGET\n/all\n<clientId>\n<ts>\n<nonce>\n<sha256(body)>` (HMAC-SHA256 hex) into `X-Auth-*` and call
`api.codersoft.xyz/all`; check the `x-api-router-upstream` header (`e3` vs `fallback`).
- **Asset pipeline:** catalog = dir tree of `Renderdragonorg/resources_renderdragon`; item fields
`{id,title,credit,filename,ext,url,size,subcategory}`. Credits come from the site's resource exports
(by repo path/filename) plus the `<name>__<credit>` filename convention (`credits.json` on e3). File
`url`s point at `hamburger.codersoft.xyz/<path>`.

## Still-open / config-only (as of last touch)

- `VITE_TURNSTILE_SITE_KEY` must be set in the deploy env for anonymous asset sessions (logged-in users
bypass Turnstile via their Supabase token).
- OIDC asymmetric signing keys are not yet enabled in Supabase. `standardScopesFor` requests
`openid` (per `oauth-consent.md`), which needs RS256/ES256 signing keys enabled before ID tokens work.
- Demo OAuth clients should be deleted.
- The site must be deployed from this branch for production to call the gateway.
78 changes: 78 additions & 0 deletions api/avatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Same-origin avatar proxy.
//
// Third-party avatar hosts (Google, GitHub, Discord) are routinely blocked by ad/tracker
// blockers and privacy DNS, which leaves users with a blank avatar. Serving them from our
// own origin sidesteps that entirely and keeps the image request first-party.
//
// SSRF guard: only a fixed allowlist of avatar hosts may be fetched, and redirects are
// never followed.

const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};

const ALLOWED_AVATAR_HOSTS = new Set([
'lh3.googleusercontent.com',
'lh4.googleusercontent.com',
'lh5.googleusercontent.com',
'lh6.googleusercontent.com',
'avatars.githubusercontent.com',
'cdn.discordapp.com',
'media.discordapp.net',
]);

export function isAllowedAvatarUrl(rawUrl) {
let url;
try {
url = new URL(rawUrl);
} catch {
return false;
}
if (url.protocol !== 'https:' && url.protocol !== 'http:') return false;
return ALLOWED_AVATAR_HOSTS.has(url.hostname.toLowerCase());
}

export default async function handler(req) {
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders });
}

try {
const target = new URL(req.url, `http://${req.headers.host}`).searchParams.get('u');
if (!target) {
return new Response('Missing avatar url', { status: 400, headers: corsHeaders });
}
if (!isAllowedAvatarUrl(target)) {
return new Response('URL not allowed', { status: 400, headers: corsHeaders });
}

// redirect: 'manual' — never follow redirects (they could point at internal hosts)
const upstream = await fetch(target, { redirect: 'manual' });
if (upstream.status >= 300 && upstream.status < 400) {
return new Response('Redirect not allowed', { status: 502, headers: corsHeaders });
}
if (!upstream.ok) {
return new Response('Upstream error', { status: 502, headers: corsHeaders });
}

const contentType = upstream.headers.get('content-type') || '';
if (!contentType.startsWith('image/')) {
return new Response('Not an image', { status: 502, headers: corsHeaders });
}

return new Response(upstream.body, {
status: 200,
headers: {
...corsHeaders,
'Content-Type': contentType,
// Avatars change rarely; cache hard at the edge and lightly in the browser.
'Cache-Control': 'public, max-age=86400, s-maxage=604800, stale-while-revalidate=86400',
},
});
} catch (error) {
console.error('Avatar proxy error:', error);
return new Response('Avatar proxy error', { status: 502, headers: corsHeaders });
}
}
2 changes: 2 additions & 0 deletions api/cors.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const allowedOrigins = new Set([
'http://127.0.0.1:3000',
'https://renderdragon.org',
'https://www.renderdragon.org',
'https://api.codersoft.xyz',
'https://assets.codersoft.xyz',
'https://assets-api-worker.powernplant101-c6b.workers.dev',
]);

Expand Down
14 changes: 12 additions & 2 deletions api/stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ const FALLBACK_ADMIN_EMAILS = [

const GRANULARITIES = new Set(['day', 'week', 'month']);

// The local Express adapter in server.js passes plain objects, while Vercel
// passes a real Request; support both shapes.
function getHeader(request, name) {
const headers = request.headers;
if (!headers) return null;
if (typeof headers.get === 'function') return headers.get(name);
const lower = name.toLowerCase();
return headers[lower] ?? headers[name] ?? null;
}

function adminEmails() {
const configured = (process.env.ANALYTICS_ADMIN_EMAILS || '')
.split(',')
Expand All @@ -18,7 +28,7 @@ function adminEmails() {
}

function corsHeaders(request) {
const origin = request.headers.get('origin');
const origin = getHeader(request, 'origin');
const headers = {
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
Expand Down Expand Up @@ -58,7 +68,7 @@ export async function handler(request) {
return json({ error: 'server not configured' }, 500, headers);
}

const authHeader = request.headers.get('authorization') || '';
const authHeader = getHeader(request, 'authorization') || '';
if (!authHeader.toLowerCase().startsWith('bearer ')) {
return json({ error: 'unauthorized' }, 401, headers);
}
Expand Down
37 changes: 27 additions & 10 deletions api/track.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,30 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
const BOT_RE =
/bot|crawl|spider|slurp|bingpreview|headless|lighthouse|pingdom|uptime|monitor|facebookexternalhit|whatsapp|telegram|discordbot|preview/i;

// The local Express adapter in server.js passes plain objects, while Vercel
// passes a real Request; support both shapes.
function getHeader(request, name) {
const headers = request.headers;
if (!headers) return null;
if (typeof headers.get === 'function') return headers.get(name);
const lower = name.toLowerCase();
return headers[lower] ?? headers[name] ?? null;
}

async function readJsonBody(request) {
if (request.body && typeof request.body === 'object') return request.body;
if (typeof request.json === 'function') {
try {
return await request.json();
} catch {
return {};
}
}
return {};
}

function corsHeaders(request) {
const origin = request.headers.get('origin');
const origin = getHeader(request, 'origin');
const headers = {
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
Expand Down Expand Up @@ -38,7 +60,7 @@ function supabaseConfig() {
}

async function resolveUser(url, anonKey, request) {
const header = request.headers.get('authorization') || request.headers.get('Authorization');
const header = getHeader(request, 'authorization');
if (!header || !header.toLowerCase().startsWith('bearer ')) return null;
const accessToken = header.slice(7).trim();
if (!accessToken) return null;
Expand Down Expand Up @@ -66,15 +88,10 @@ export async function handler(request) {
});
}

const userAgent = request.headers.get('user-agent') || '';
const userAgent = getHeader(request, 'user-agent') || '';
if (!userAgent || BOT_RE.test(userAgent)) return new Response(null, { status: 204, headers });

let payload = {};
try {
payload = await request.json();
} catch {
payload = {};
}
const payload = await readJsonBody(request);

const browserId = truncate(payload?.browserId, 64);
if (!browserId || !UUID_RE.test(browserId)) return new Response(null, { status: 204, headers });
Expand All @@ -88,7 +105,7 @@ export async function handler(request) {
try {
const user = anonKey ? await resolveUser(url, anonKey, request) : null;
const visitorKey = user ? `u:${user.id}` : `b:${browserId}`;
const country = request.headers.get('x-vercel-ip-country') || null;
const country = getHeader(request, 'x-vercel-ip-country') || null;

const admin = createClient(url, secretKey, {
auth: { autoRefreshToken: false, persistSession: false },
Expand Down
Loading
Loading