Add Wave 1 block server schemas (page builder step 3, server half)
Register all seven Wave 1 block types with their server-side validation schemas, self-registering via server/src/blocks/types/*: heading, rich_text, image, two_column (container), cta, divider, quote. - propHelpers.js: shared validators (isSafeUrl rejects javascript:/data:/ protocol-relative, enum/required/optional text, strict key allowlist). - rich_text carries a `sanitize` normalizer (registry now supports it) that runs html through the shared cleanBody allowlist on save. - Registry entrypoint requires the type modules so all schemas load. Verified: all 7 register; valid blocks pass; malformed props yield precise per-path errors; one-level nesting cap enforced; rich_text sanitize strips script/onerror. Client renderers + editors (step 3 client half) still to come. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
84
server/src/blocks/propHelpers.js
Normal file
84
server/src/blocks/propHelpers.js
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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,
|
||||
}
|
||||
Reference in New Issue
Block a user