An API you can curl, or open in a browser, to receive Bluesky data as markdown!
1import { NextRequest, NextResponse } from 'next/server'
2
3// ---------------------------------------------------------------------------
4// Rate limiting — edge-side, in-memory sliding window
5// Each edge instance tracks its own state. Not globally coordinated, but
6// sufficient to cap runaway single-IP abuse and protect Bluesky's API.
7// ---------------------------------------------------------------------------
8const WINDOW_MS = parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? '60000', 10)
9const MAX_REQUESTS = parseInt(process.env.RATE_LIMIT_MAX ?? '10', 10)
10
11const ipWindows = new Map<string, number[]>()
12
13function isRateLimited(ip: string): boolean {
14 const now = Date.now()
15 const hits = (ipWindows.get(ip) ?? []).filter(t => now - t < WINDOW_MS)
16 if (hits.length >= MAX_REQUESTS) return true
17 hits.push(now)
18 ipWindows.set(ip, hits)
19 return false
20}
21
22// ---------------------------------------------------------------------------
23// Terminal-client detection (curl, wget, etc.)
24// ---------------------------------------------------------------------------
25function isTerminalClient(req: NextRequest): boolean {
26 const ua = req.headers.get('user-agent') ?? ''
27 const accept = req.headers.get('accept') ?? ''
28
29 if (/^(curl|Wget|HTTPie|httpie|xh\/|python-httpx|python-requests|Go-http-client|nushell|Nu\/|httpx\/)/i.test(ua)) {
30 return true
31 }
32 if (/^text\/(markdown|plain)/.test(accept)) return true
33 if (ua && !ua.includes('Mozilla') && !accept.includes('text/html')) {
34 return true
35 }
36 return false
37}
38
39// ---------------------------------------------------------------------------
40// Middleware
41// ---------------------------------------------------------------------------
42export function middleware(req: NextRequest) {
43 const { pathname } = req.nextUrl
44
45 // Terminal rewrite on homepage
46 if (pathname === '/' && isTerminalClient(req)) {
47 const url = req.nextUrl.clone()
48 url.pathname = '/cli'
49 return NextResponse.rewrite(url)
50 }
51
52 // Rate-limit API routes
53 if (pathname.startsWith('/profile') || pathname === '/search' || pathname === '/trending') {
54 const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
55 if (isRateLimited(ip)) {
56 return new NextResponse('Rate limit exceeded. Please slow down.\n', {
57 status: 429,
58 headers: {
59 'Content-Type': 'text/plain; charset=utf-8',
60 'Retry-After': '60',
61 },
62 })
63 }
64 }
65
66 return NextResponse.next()
67}
68
69export const config = {
70 matcher: ['/', '/profile/:path*', '/search', '/trending'],
71}