okr/lib/okf-links.mjs

39 lines
1.9 KiB
JavaScript

// okf-links.mjs
// Delt bundle-root-relativ lenke-resolver (Step 6), brukt av BAADE relasjons-
// emitteringen (innboks-relations) og okf-check++ --strict-ingest (Step 8).
// Konvensjon: en trygg bundle-lenke er ROOT-RELATIV med leading '/' (resolveres
// mot bundle-rota, jf. SC-regex \]\(/.*\.md\)). Avviser '../'-escape, absolutt
// Windows-sti (backslash), og scheme-lenker (file://, http(s)://). Den ENESTE
// lenke-allow-listen -- delt slik at emit og validering aldri divergerer.
// Zero npm dependencies.
import path from 'node:path';
// Scheme-prefiks (file://, http://, https://, ...) -- avvises uansett.
const SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
// '..'-segment hvor som helst i stien (escape ut av bundle-rota).
const PARENT_SEGMENT_RE = /(^|\/)\.\.(\/|$)/;
// Sann hvis target er en trygg, bundle-root-relativ .md-lenke (leading '/').
export function isSafeBundleLink(target) {
if (typeof target !== 'string' || target === '') return false;
if (target.includes('\0')) return false; // null-byte
if (target.includes('\\')) return false; // backslash (Windows-sti)
if (SCHEME_RE.test(target)) return false; // file://, http(s)://, ...
if (!target.startsWith('/')) return false; // maa vaere bundle-root-relativ
if (PARENT_SEGMENT_RE.test(target)) return false; // ingen '..'-escape
if (!target.endsWith('.md')) return false; // bundle-konseptlenker er .md
return true;
}
// Resolverer en trygg bundle-lenke til absolutt sti UNDER bundleRoot, ellers null.
// Belt-and-suspenders: isSafeBundleLink avviser '..' allerede, men confinement
// re-sjekkes paa den resolverte stien.
export function resolveBundleLink(target, bundleRoot) {
if (!isSafeBundleLink(target)) return null;
const rel = target.replace(/^\/+/, '');
const root = path.resolve(bundleRoot);
const resolved = path.resolve(root, rel);
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
return resolved;
}