feat: productionize learning engine and classroom
This commit is contained in:
@@ -1,74 +1,31 @@
|
||||
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.
|
||||
* The Learning Engine exposes one token-protected runtime surface. Original
|
||||
* browser APIs remain callable internally by runtime route handlers, but are
|
||||
* never reachable as unauthenticated ClusterIP endpoints.
|
||||
*/
|
||||
export function shouldReturnNotFoundAtServerBoundary(
|
||||
role: DeploymentRole,
|
||||
pathname: string,
|
||||
method = 'GET',
|
||||
): boolean {
|
||||
if (role !== 'server') {
|
||||
if (role !== 'server') return false;
|
||||
const normalized = withoutTrailingSlash(pathname);
|
||||
if (!isApiPath(normalized)) return true;
|
||||
if (normalized === '/api/runtime/v1' || normalized.startsWith('/api/runtime/v1/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedPathname = withoutTrailingSlash(pathname);
|
||||
|
||||
if (!isApiPath(normalizedPathname)) {
|
||||
return true;
|
||||
if (normalized === '/api/internal/course-publish' && method.toUpperCase() === 'POST') {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user