102 lines
3.9 KiB
JavaScript
102 lines
3.9 KiB
JavaScript
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,
|
|
};
|