-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmiddleware.ts
More file actions
152 lines (133 loc) · 5.21 KB
/
middleware.ts
File metadata and controls
152 lines (133 loc) · 5.21 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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { createRateLimiter } from './src/lib/rate-limit-in-memory';
/**
* Block direct static access to brain-map JSON under /public — use GET /api/brain-map/graph only
* (optional BRAIN_MAP_SECRET + x-brain-map-key).
*/
const BRAIN_MAP_STATIC_PATHS = new Set([
'/brain-map-graph.json',
'/brain-map-graph.local.json',
]);
/** POST /api/survey — single Node instance; not for multi-replica. */
const rateLimitSyncSessionSubmit = createRateLimiter(60_000, 30);
/** POST /api/auth/login — stricter; brute-force protection (per-process only). */
const rateLimitLogin = createRateLimiter(60_000, 10);
/** POST /api/operator-probes/ingest — runner + operator ingest (per-process only). */
const rateLimitOperatorProbeIngest = createRateLimiter(60_000, 30);
/**
* GET discovery / OpenAPI — generous per-IP limit to reduce scraping noise (single Node; not multi-replica).
* 200 requests / minute / IP (same window as other limiters).
*/
const rateLimitDiscoveryGet = createRateLimiter(60_000, 200);
const DISCOVERY_GET_PATHS = new Set(['/api/capabilities', '/api/openapi', '/api/openapi.json']);
/**
* Dev/demo App Router pages only (OA-4). Blocked in production unless explicitly allowed
* (e.g. staging). See .env.example OPENGRIMOIRE_ALLOW_TEST_ROUTES.
*
* Maintainer: keep in sync with `export const config.matcher` at the bottom of this file —
* every prefix here must have matching matcher entries (Next.js path patterns). `isTestDevRoute`
* treats `pathname === prefix` or `pathname.startsWith(prefix + '/')`. When adding a prefix,
* update matcher + `e2e/test-routes.spec.ts` smoke for that route.
*/
const TEST_ROUTE_PREFIXES = ['/test', '/test-chord', '/test-context', '/test-sqlite'] as const;
function isTestDevRoute(pathname: string): boolean {
return TEST_ROUTE_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
}
function testRoutesAllowedInThisDeployment(): boolean {
if (process.env.NODE_ENV !== 'production') {
return true;
}
const v = process.env.OPENGRIMOIRE_ALLOW_TEST_ROUTES;
return v === '1' || v === 'true';
}
function getClientIp(req: NextRequest): string {
return (
req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
req.headers.get('x-real-ip') ||
'unknown'
);
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (isTestDevRoute(pathname) && !testRoutesAllowedInThisDeployment()) {
return new NextResponse(
`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"/><title>Not found</title></head><body><h1>Not found</h1><p>Dev-only routes are disabled in this deployment. To allow (staging only), set <code>OPENGRIMOIRE_ALLOW_TEST_ROUTES=1</code>. See <code>.env.example</code>.</p></body></html>`,
{ status: 404, headers: { 'Content-Type': 'text/html; charset=utf-8' } }
);
}
if (BRAIN_MAP_STATIC_PATHS.has(pathname)) {
return NextResponse.json(
{
error: 'Not found',
detail:
'Brain map JSON is served only via GET /api/brain-map/graph (see docs/AGENT_INTEGRATION.md).',
},
{ status: 404 }
);
}
if (pathname === '/api/survey' && request.method === 'POST') {
const ip = getClientIp(request);
if (!rateLimitSyncSessionSubmit(ip)) {
return NextResponse.json(
{ error: 'Too many requests', detail: 'Sync Session submit rate limit exceeded. Try again later.' },
{ status: 429, headers: { 'Retry-After': '60' } }
);
}
}
if (pathname === '/api/auth/login' && request.method === 'POST') {
const ip = getClientIp(request);
if (!rateLimitLogin(ip)) {
return NextResponse.json(
{ error: 'Too many requests', detail: 'Login rate limit exceeded. Try again later.' },
{ status: 429, headers: { 'Retry-After': '60' } }
);
}
}
if (pathname === '/api/operator-probes/ingest' && request.method === 'POST') {
const ip = getClientIp(request);
if (!rateLimitOperatorProbeIngest(ip)) {
return NextResponse.json(
{
error: 'Too many requests',
detail: 'Operator probe ingest rate limit exceeded. Try again later.',
},
{ status: 429, headers: { 'Retry-After': '60' } }
);
}
}
if (request.method === 'GET' && DISCOVERY_GET_PATHS.has(pathname)) {
const ip = getClientIp(request);
if (!rateLimitDiscoveryGet(ip)) {
return NextResponse.json(
{
error: 'Too many requests',
detail: 'Discovery endpoint rate limit exceeded. Try again later.',
},
{ status: 429, headers: { 'Retry-After': '60' } }
);
}
}
return NextResponse.next();
}
/** Must cover every `TEST_ROUTE_PREFIXES` entry (OA-4). Drift = middleware never runs for a dev route. */
export const config = {
matcher: [
'/brain-map-graph.json',
'/brain-map-graph.local.json',
'/api/survey',
'/api/auth/login',
'/api/operator-probes/ingest',
'/api/capabilities',
'/api/openapi',
'/api/openapi.json',
'/test',
'/test/:path*',
'/test-chord',
'/test-chord/:path*',
'/test-context',
'/test-context/:path*',
'/test-sqlite',
'/test-sqlite/:path*',
],
};