feat(engagement): templates — the email block family, renderer and seeded set (engagement Phase 5a)
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>
This commit is contained in:
@@ -4,44 +4,58 @@
|
||||
// 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')
|
||||
|
||||
function sanitizeBlocks(blocks) {
|
||||
if (!Array.isArray(blocks)) return []
|
||||
return blocks.map(sanitizeOne)
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
|
||||
function sanitizeOne(block) {
|
||||
const def = getBlock(block.type)
|
||||
if (!def) return block // unreachable after validation, but stay defensive
|
||||
let props = block.props && typeof block.props === 'object' ? { ...block.props } : {}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { sanitizeBlocks }
|
||||
// The page-registry binding — the export every existing caller already uses.
|
||||
const sanitizeBlocks = makeSanitizeBlocks(getBlock)
|
||||
|
||||
module.exports = { sanitizeBlocks, makeSanitizeBlocks }
|
||||
|
||||
Reference in New Issue
Block a user