Repository navigation
Expand file tree
/
Copy pathmiddleware.ts
More file actions
90 lines (74 loc) · 3.05 KB
/
Copy pathmiddleware.ts
File metadata and controls
90 lines (74 loc) · 3.05 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
9F98
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
import { NextRequest, NextResponse } from 'next/server';
import { isAgentRuntimeConfigured, isProWorkbenchEnabled } from '@/lib/config/feature-flags';
/** Convert string to Uint8Array */
function encode(str: string): Uint8Array {
return new TextEncoder().encode(str);
}
/** Convert ArrayBuffer to hex string */
function bufToHex(buf: ArrayBuffer): string {
return Array.from(new Uint8Array(buf))
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
/** Verify an HMAC-signed token using Web Crypto API (Edge-compatible) */
async function verifyToken(token: string, accessCode: string): Promise<boolean> {
const dotIndex = token.indexOf('.');
if (dotIndex === -1) return false;
const timestamp = token.substring(0, dotIndex);
const signature = token.substring(dotIndex + 1);
const keyData = encode(accessCode);
const key = await crypto.subtle.importKey(
'raw',
keyData.buffer as ArrayBuffer,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign'],
);
const data = encode(timestamp);
const expected = bufToHex(await crypto.subtle.sign('HMAC', key, data.buffer as ArrayBuffer));
// Constant-length comparison (not truly constant-time in JS, but sufficient here)
if (signature.length !== expected.length) return false;
let mismatch = 0;
for (let i = 0; i < signature.length; i++) {
mismatch |= signature.charCodeAt(i) ^ expected.charCodeAt(i);
}
return mismatch === 0;
}
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Return an actual server-side 404 when either half of the workbench is off.
// Edge middleware cannot reliably inspect server-only deployment variables,
// so it enforces the public gate and leaves the complete runtime/database
// check to Node. A Node-hosted middleware uses the same gate as startup.
const canInspectServerRuntime = process.env.NEXT_RUNTIME !== 'edge';
const workbenchEnabled =
isProWorkbenchEnabled() && (!canInspectServerRuntime || isAgentRuntimeConfigured());
if (!workbenchEnabled && (pathname === '/workbench' || pathname.startsWith('/workbench/'))) {
return new NextResponse('Not found', { status: 404 });
}
const accessCode = process.env.ACCESS_CODE;
if (!accessCode) {
return NextResponse.next();
}
// Whitelist: access-code endpoints, health check
if (pathname.startsWith('/api/access-code/') || pathname === '/api/health') {
return NextResponse.next();
}
// Check cookie — validate HMAC signature, not just existence
const cookie = request.cookies.get('openmaic_access');
if (cookie?.value && (await verifyToken(cookie.value, accessCode))) {
return NextResponse.next();
}
// API requests without valid cookie → 401
if (pathname.startsWith('/api/')) {
return NextResponse.json(
{ success: false, errorCode: 'INVALID_REQUEST', error: 'Access code required' },
{ status: 401 },
);
}
// Page requests → let through, frontend shows modal
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|logos/).*)'],
};