75 lines
2.4 KiB
TypeScript
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;
|
|
}
|