48 lines
2.7 KiB
TypeScript
48 lines
2.7 KiB
TypeScript
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}
|
|
}
|