Files
openmaic/OpenMAIC/lib/config/server-request-boundary.ts
2026-08-16 14:58:47 +08:00

75 lines
2.4 KiB
TypeScript

import type { DeploymentRole } from '@/lib/config/deployment-role';
const SERVER_HIDDEN_API_ROOTS = [
'/api/courses',
'/api/ops',
'/api/access-code',
// This browser-side CORS convenience proxy has no authenticated server-tier
// contract. Keeping it off the headless public API also closes DNS-rebinding
// and cloud-metadata SSRF paths until learner identity is introduced.
'/api/proxy-media',
'/api/usage',
'/api/provider/probe-models',
'/api/azure-voices',
'/api/verify-model',
'/api/verify-image-provider',
'/api/verify-video-provider',
'/api/verify-pdf-provider',
'/api/export-video',
// The current persistence token is a development convenience compiled into
// the client. It is not a tenant/owner boundary and must stay off L2.
'/api/persistence',
] as const;
export function isApiPath(pathname: string): boolean {
return pathname === '/api' || pathname.startsWith('/api/');
}
function isPathFamily(pathname: string, root: string): boolean {
return pathname === root || pathname.startsWith(`${root}/`);
}
function withoutTrailingSlash(pathname: string): string {
return pathname.length > 1 ? pathname.replace(/\/+$/, '') : pathname;
}
/**
* A server deployment is a headless data/API service. It deliberately hides
* every page plus APIs owned by the operations workbench. Other APIs continue
* to their route-level authentication until learner ownership is defined.
*/
export function shouldReturnNotFoundAtServerBoundary(
role: DeploymentRole,
pathname: string,
method = 'GET',
): boolean {
if (role !== 'server') {
return false;
}
const normalizedPathname = withoutTrailingSlash(pathname);
if (!isApiPath(normalizedPathname)) {
return true;
}
if (SERVER_HIDDEN_API_ROOTS.some((root) => isPathFamily(normalizedPathname, root))) {
return true;
}
const normalizedMethod = method.toUpperCase();
if (
normalizedPathname === '/api/generate-classroom' &&
(normalizedMethod === 'GET' || normalizedMethod === 'HEAD')
) {
// A global job list has no owner filter. Per-job and POST generation stay
// available until the learner capability contract is introduced. HEAD is
// GET-equivalent in Next.js and must not bypass this list boundary.
return true;
}
if (normalizedPathname === '/api/internal/course-publish' && normalizedMethod !== 'POST') {
return true;
}
return false;
}