export type RouteAccessDecision = | { type: 'allow' } | { type: 'login'; redirect: string } | { type: 'forbidden'; from: string } export interface RouteAccessInput { requiresAuth?: boolean isAuthenticated: boolean fullPath: string permission?: string hasPermission: boolean } export interface PreviewTargetInput { currentPath: string currentPermission?: string candidateCodes: ReadonlySet firstPath: string | null } export function sanitizeInternalPath(value: unknown): string { if ( typeof value !== 'string' || !value.startsWith('/') || value[1] === '/' || value[1] === '\\' || /[\u0000-\u001F\u007F]/.test(value) ) return '/' return value } export function decideRouteAccess(input: RouteAccessInput): RouteAccessDecision { if (input.requiresAuth && !input.isAuthenticated) { return { type: 'login', redirect: sanitizeInternalPath(input.fullPath) } } if (input.permission && !input.hasPermission) { return { type: 'forbidden', from: sanitizeInternalPath(input.fullPath) } } return { type: 'allow' } } export function resolvePreviewTarget(input: PreviewTargetInput): string | null { if (input.currentPath === '/403') return input.firstPath ?? '/403' if (input.currentPermission && !input.candidateCodes.has(input.currentPermission)) { return input.firstPath ?? '/403' } return null }