// Small shared validators used by the Wave 1 block schemas. Each block's schema // composes these and returns a flat array of error strings; validateBlocks // prefixes each with the block path (so 'text is required' becomes // 'blocks[2].props.text is required'). Phrase messages to read well after that // prefix — start with the prop name. /** @returns {boolean} true if v is a non-empty (after trim) string. */ function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0 } /** * Accept a same-origin relative URL ("/uploads/x.png", "/wiki/foo") or an * absolute http/https URL. Rejects javascript:, data:, protocol-relative * ("//evil"), and anything else — the block renderers drop these into hrefs/src * so this is a security boundary, not just a format check. * @param {unknown} v * @returns {boolean} */ function isSafeUrl(v) { if (typeof v !== 'string' || v.trim() === '') return false const s = v.trim() if (s.startsWith('//')) return false // protocol-relative — ambiguous origin if (s.startsWith('/')) return true // same-origin relative try { const u = new URL(s) return u.protocol === 'http:' || u.protocol === 'https:' } catch { return false } } /** * Build an enum validator for a prop. * @param {string} name prop name (for the message) * @param {string[]} allowed * @returns {(v: unknown) => string|null} error string or null */ function oneOf(name, allowed) { return (v) => (allowed.includes(v) ? null : `${name} must be one of ${allowed.join(', ')}`) } /** * Validate a required text prop: present, non-empty, within maxLen. * @returns {string|null} */ function requiredText(name, v, maxLen) { if (!isNonEmptyString(v)) return `${name} is required` if (v.length > maxLen) return `${name} must be at most ${maxLen} characters` return null } /** * Validate an optional text prop: if present it must be a string within maxLen. * @returns {string|null} */ function optionalText(name, v, maxLen) { if (v === undefined || v === null || v === '') return null if (typeof v !== 'string') return `${name} must be a string` if (v.length > maxLen) return `${name} must be at most ${maxLen} characters` return null } /** * Reject any prop key not in `allowed`. Keeps a block's props tight so nothing * unexpected is smuggled through and stored. * @returns {string[]} error strings */ function onlyKeys(props, allowed) { const errors = [] for (const key of Object.keys(props)) { if (!allowed.includes(key)) errors.push(`${key} is not an allowed prop`) } return errors } module.exports = { isNonEmptyString, isSafeUrl, oneOf, requiredText, optionalText, onlyKeys, }