export const MINDSPACE_PUBLIC_ROOT = 'MindSpace'; export const MINDSPACE_PUBLIC_ZONE = 'public'; function urlError(message, code, details = {}) { return Object.assign(new Error(message), { code, ...details }); } function requiredSegment(value, field) { const normalized = String(value ?? '').trim(); if (!normalized) { throw urlError(`${field} is required`, 'invalid_mindspace_url_segment', { field }); } if (normalized.includes('/') || normalized.includes('\\') || normalized.includes('\0')) { throw urlError(`${field} must be a single URL segment`, 'invalid_mindspace_url_segment', { field, }); } return normalized; } export function normalizeMindSpacePublicBaseUrl(publicBaseUrl) { const normalized = String(publicBaseUrl ?? '').trim().replace(/\/+$/, ''); if (!normalized) { throw urlError('publicBaseUrl is required', 'invalid_mindspace_public_base_url'); } return normalized; } export function normalizeMindSpacePublicRelativePath(relativePath) { const raw = String(relativePath ?? '').trim(); if (!raw) { throw urlError('relativePath is required', 'invalid_mindspace_public_relative_path'); } if (raw.includes('\0') || raw.includes('\\')) { throw urlError('relativePath must use URL-style forward slashes', 'invalid_mindspace_public_relative_path'); } if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(raw) || raw.startsWith('//')) { throw urlError('relativePath must not be an absolute URL', 'invalid_mindspace_public_relative_path'); } const clean = raw.replace(/^\/+/, ''); const parts = clean.split('/').filter(Boolean); if (parts.length === 0 || parts.includes('..')) { throw urlError('relativePath must not traverse parent directories', 'invalid_mindspace_public_relative_path'); } return parts.join('/'); } export function createMindSpacePublicUrl({ publicBaseUrl, ownerKey, relativePath }) { const base = normalizeMindSpacePublicBaseUrl(publicBaseUrl); const owner = requiredSegment(ownerKey, 'ownerKey'); const relative = normalizeMindSpacePublicRelativePath(relativePath); const encodedRelative = relative.split('/').map(encodeURIComponent).join('/'); return `${base}/${MINDSPACE_PUBLIC_ROOT}/${encodeURIComponent(owner)}/${encodedRelative}`; } export function createMindSpaceUserRootUrl({ publicBaseUrl, ownerKey }) { const base = normalizeMindSpacePublicBaseUrl(publicBaseUrl); const owner = requiredSegment(ownerKey, 'ownerKey'); return `${base}/${MINDSPACE_PUBLIC_ROOT}/${encodeURIComponent(owner)}/`; } export function createMindSpacePublicPageUrl({ publicBaseUrl, ownerKey, filename }) { const clean = normalizeMindSpacePublicRelativePath(filename); const relative = clean.startsWith(`${MINDSPACE_PUBLIC_ZONE}/`) ? clean : `${MINDSPACE_PUBLIC_ZONE}/${clean}`; return createMindSpacePublicUrl({ publicBaseUrl, ownerKey, relativePath: relative }); } export function parseMindSpacePublicUrl(inputUrl) { const parsed = new URL(String(inputUrl ?? ''), 'https://mindspace.invalid'); const parts = parsed.pathname.split('/').filter(Boolean).map(decodeURIComponent); const rootIndex = parts.indexOf(MINDSPACE_PUBLIC_ROOT); if (rootIndex < 0 || parts.length < rootIndex + 3) { throw urlError('URL is not a MindSpace public URL', 'not_mindspace_public_url'); } const ownerKey = parts[rootIndex + 1]; const relativePath = normalizeMindSpacePublicRelativePath(parts.slice(rootIndex + 2).join('/')); return { ownerKey, relativePath, isPublicZone: relativePath === MINDSPACE_PUBLIC_ZONE || relativePath.startsWith(`${MINDSPACE_PUBLIC_ZONE}/`), filename: relativePath.split('/').at(-1) ?? '', }; } export function canonicalizeMindSpacePublicUrl(inputUrl, { publicBaseUrl }) { const parsed = parseMindSpacePublicUrl(inputUrl); return createMindSpacePublicUrl({ publicBaseUrl, ownerKey: parsed.ownerKey, relativePath: parsed.relativePath, }); } export const mindspaceCanonicalUrlInternals = { requiredSegment, urlError, };