Every subject and body moves out of `mailer.js` into `engagement_templates` rows an operator can edit. A relocation, not a regression: nothing that sends mail today starts depending on an operator authoring something first. - `email.*` block family in its own registry, sharing the page family's envelope walk and validate-then-sanitize order by binding rather than by copy. - A server-side renderer producing both parts of a multipart message; the text part is byte-identical to the literals this commit deletes. - Nine seeded templates, six of them wired now; the seeder's `customized = 0` guard lives in the UPDATE's own WHERE. - `renderByKey` falls back to the shipped seed when a row is missing or unusable, so no failure of the table can stop a password reset. Also fixes `check:hosts` reading the template key `auth.email-verify` as the hostname `auth.email`. Co-Authored-By: Claude <noreply@anthropic.com>
62 lines
2.2 KiB
JavaScript
62 lines
2.2 KiB
JavaScript
// Normalize + sanitize a validated blocks array before persisting. Runs AFTER
|
|
// validateBlocks (which guarantees the envelope/prop shape), so this can assume
|
|
// well-formed input and focus on: applying each block's registry `sanitize`
|
|
// normalizer (e.g. rich_text runs its html through the allowlist), stamping the
|
|
// registry `version`, defaulting `visible` to true, and recursing one level into
|
|
// container slots. Returns a new array; never mutates the input.
|
|
//
|
|
// Parameterized by a registry lookup for the same reason validateBlocks is
|
|
// (engagement Phase 5a): the `email.*` family is a separate registry and must get
|
|
// the same validate-then-sanitize order, not a second implementation of it.
|
|
|
|
const { getBlock } = require('./registry')
|
|
|
|
/**
|
|
* Build a blocks sanitizer bound to one registry.
|
|
* @param {(type: string) => object|null} lookup registry `getBlock`
|
|
* @returns {(blocks: unknown) => object[]}
|
|
*/
|
|
function makeSanitizeBlocks(lookup) {
|
|
function sanitizeOne(block) {
|
|
const def = lookup(block.type)
|
|
if (!def) return block // unreachable after validation, but stay defensive
|
|
|
|
let props = block.props && typeof block.props === 'object' ? { ...block.props } : {}
|
|
|
|
// Recurse into container slots first (leaf sub-blocks get sanitized too).
|
|
if (def.container) {
|
|
for (const slot of def.containerSlots) {
|
|
if (Array.isArray(props[slot])) props[slot] = props[slot].map(sanitizeOne)
|
|
}
|
|
}
|
|
|
|
// Apply the block's own normalizer last (operates on its scalar props).
|
|
if (def.sanitize) {
|
|
try {
|
|
props = def.sanitize(props)
|
|
} catch {
|
|
// Leave props as-is; validation already passed, a sanitize throw shouldn't
|
|
// block the save.
|
|
}
|
|
}
|
|
|
|
return {
|
|
id: block.id,
|
|
type: block.type,
|
|
version: Number.isInteger(block.version) ? block.version : def.version,
|
|
visible: block.visible !== false,
|
|
props,
|
|
}
|
|
}
|
|
|
|
return function sanitizeBlocks(blocks) {
|
|
if (!Array.isArray(blocks)) return []
|
|
return blocks.map(sanitizeOne)
|
|
}
|
|
}
|
|
|
|
// The page-registry binding — the export every existing caller already uses.
|
|
const sanitizeBlocks = makeSanitizeBlocks(getBlock)
|
|
|
|
module.exports = { sanitizeBlocks, makeSanitizeBlocks }
|