-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproxy.ts
More file actions
83 lines (73 loc) · 2.84 KB
/
Copy pathproxy.ts
File metadata and controls
83 lines (73 loc) · 2.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import { NextRequest, NextResponse } from 'next/server'
import { getClientIp, hasAuthSignal, rateLimit, rateLimitHeaders } from '@/lib/ratelimit'
/**
* Rate-limit public API surfaces. Keep this lightweight: only pattern-match on
* pathname, do the Redis/mem check, and pass through. No DB, no auth lookups.
*
* Buckets:
* - anon -> 60 req/min per IP (matches OPE-24 L1 spec)
* - authed -> 300 req/min per identity (API key / Supabase auth cookie)
*
* We intentionally do NOT rate-limit admin endpoints here because those already
* require auth and have small audiences; a broken limiter would lock out ops.
*/
const WINDOW_SEC = 60
const ANON_LIMIT = 60
const AUTHED_LIMIT = 300
export async function proxy(req: NextRequest) {
const { pathname } = req.nextUrl
// Guard clause: matcher below already filters, but be defensive.
// Covered surfaces (OPE-24 L1 + OPE-72 retest expansion):
// - /api/public/* anonymous public data
// - /api/v1/events* open v1 API, including skill.md toolchain
// - /api/events* authenticated event list/detail/subroutes
// - /api/public-stats anonymous aggregate stats
// - /api/a2a agent-to-agent protocol entry
// - /api/agent/register anonymous agent registration
const isPublicApi =
pathname.startsWith('/api/public/') ||
pathname === '/api/v1/events' ||
pathname.startsWith('/api/v1/events/') ||
pathname === '/api/events' ||
pathname.startsWith('/api/events/') ||
pathname === '/api/public-stats' ||
pathname === '/api/a2a' ||
pathname === '/api/agent/register'
if (!isPublicApi) return NextResponse.next()
const authed = hasAuthSignal(req)
const limit = authed ? AUTHED_LIMIT : ANON_LIMIT
const bucket = authed ? 'authed' : 'anon'
// Identity key: for authed requests prefer the api key or bearer token
// (first 32 chars, never logged) so one user behind a shared NAT does not
// share quota with anonymous IPs. Anonymous requests key on IP.
let key = getClientIp(req)
if (authed) {
const apiKey = req.headers.get('x-api-key')
const auth = req.headers.get('authorization')
const token = apiKey || auth?.replace(/^Bearer\s+/i, '') || key
key = `k:${token.slice(0, 32)}`
}
const result = await rateLimit({ bucket, key, limit, windowSec: WINDOW_SEC })
const headers = rateLimitHeaders(result)
if (!result.allowed) {
return NextResponse.json(
{ error: 'Too many requests', retry_after: result.retryAfter },
{ status: 429, headers }
)
}
const res = NextResponse.next()
for (const [k, v] of Object.entries(headers)) res.headers.set(k, v)
return res
}
export const config = {
matcher: [
'/api/public/:path*',
'/api/v1/events',
'/api/v1/events/:path*',
'/api/events',
'/api/events/:path*',
'/api/public-stats',
'/api/a2a',
'/api/agent/register',
],
}