import type { KbxNavigationResolvedEntry } from '@kbx/contracts' const MAX_WORKSPACE_URL_LENGTH=2048 function normalizePathname(value:string){const clean=value.replace(/\/+$/,'')||'/';return clean.startsWith('/')?clean:`/${clean}`} function hasUnsafeEncodedPath(value:string){return value.length>MAX_WORKSPACE_URL_LENGTH || /[\u0000-\u001f\u007f]|\\|%2f|%5c|%00|%0d|%0a|%09|%2e%2e/i.test(value)} /** Match a Vue-Router-style path pattern without trusting persisted route strings. */ export function matchKbxRoutePattern(pattern:string,candidatePath:string){ if(hasUnsafeEncodedPath(candidatePath))return false const p=normalizePathname(pattern).split('/').filter(Boolean) const c=normalizePathname(candidatePath).split('/').filter(Boolean) if(p.length!==c.length)return false return p.every((segment,index)=>segment.startsWith(':') ? Boolean(c[index]) : segment===c[index]) } /** * Re-resolve an in-memory workspace path against the screen's declared route capabilities. * Query/hash are retained only for the current in-memory workspace after the pathname passes the capability check. */ export function resolveKbxSafeWorkspacePath(patterns:readonly string[],candidatePath:string,fallbackPath:string){ try{ if(hasUnsafeEncodedPath(candidatePath))return fallbackPath const candidate=new URL(candidatePath,'https://kbx.local') if(candidate.origin!=='https://kbx.local'||candidate.username||candidate.password)return fallbackPath const allowed=patterns.some(pattern=>matchKbxRoutePattern(pattern,candidate.pathname)) return allowed?`${candidate.pathname}${candidate.search}${candidate.hash}`:fallbackPath }catch{return fallbackPath} } /** * Re-resolve untrusted persisted recent navigation against the current authorized catalog entry. * Query/hash are deliberately stripped before persistence/replay so transient filters, tokens, and sensitive * values do not leak into local preference storage. Dynamic route path persistence remains explicit opt-in. */ export function resolveKbxSafeRecentPath(entry:KbxNavigationResolvedEntry, storedPath:string){ if(entry.recentPolicy!=='route')return entry.path try{ if(hasUnsafeEncodedPath(storedPath))return entry.path const base=new URL(entry.path,'https://kbx.local') const candidate=new URL(storedPath,'https://kbx.local') if(candidate.origin!==base.origin||candidate.username||candidate.password)return entry.path const basePath=base.pathname.replace(/\/$/,'')||'/' const candidatePath=candidate.pathname.replace(/\/$/,'')||'/' const within=candidatePath===basePath || (basePath!=='/' && candidatePath.startsWith(`${basePath}/`)) return within?candidate.pathname:entry.path }catch{return entry.path} }