{ "feature_id": "F-152", "agent": "security", "stage": "security_gate", "verdict": "APPROVED", "summary": "F-152 no amplía superficie de autorización ni expone nuevos endpoints públicos. No se hardcodean credenciales SMTP (se leen de store_settings vía pool); el email de confirmación reusa el patrón existente de order-status-mailer (read-only pool lookup de identity_users.email). Los emails son best-effort y no bloquean el registro/webhook. Personalización HTML se escapa (XSS-safe). El gate outcome.kind==='processed' previene envíos duplicados ante replay de webhooks.", "evidence": [ "No se añaden endpoints públicos nuevos: POST /auth/register y POST /payments/webhook ya existían", "No se modifica RBAC ni requireRole en ningún route; register sigue siendo público (comportamiento previsto)", "SMTP credentials no aparecen en código: SettingsWelcomeMailer.readSmtpOptions lee store_settings (smtp_host/port/secure/user/pass/from) vía pg pool", "Order confirmation email lookup es read-only pool (orders_orders.user_id -> identity_users.email), mismo patrón que sendOrderStatusEmail/order-status-mailer", "Best-effort: welcome .catch(request.log.warn); order confirmation en try/catch con warn -> no excepción al cliente", "Idempotencia webhook: outcome.kind==='processed' garantiza un solo email por PaymentSucceeded (PaymentsService devuelve kind:'duplicate' antes de transición)", "XSS-safe: buildWelcomeEmail HTML-escapea name (test unitario)", "No new data at rest; no new tokens/secrets" ], "checks": [ {"item": "Autorización / RBAC intacta", "ok": true, "evidence": "No se modifican requireRole ni middlewares de auth; register sigue público, webhook sigue sin auth (preexistente)"}, {"item": "No credenciales SMTP en código", "ok": true, "evidence": "SettingsWelcomeMailer readSmtpOptions(pool) consulta store_settings; prettier/eslint no detectan secrets; git diff --check limpio"}, {"item": "Lookup de email cliente read-only", "ok": true, "evidence": "payments.routes usa pool.query SELECT identity_users.email FROM orders_orders o JOIN identity_users; no writes"}, {"item": "Best-effort no expone errores internos", "ok": true, "evidence": "welcome .catch(warn); order-conf try/catch warn; registro/webhook nunca propagan error SMTP al cliente"}, {"item": "Idempotencia frente a replay", "ok": true, "evidence": "gate outcome.kind==='processed' + PaymentsService kind:'duplicate'"}, {"item": "XSS en personalización", "ok": true, "evidence": "buildWelcomeEmail escapea name (test)"}, {"item": "Data at rest / secrets", "ok": true, "evidence": "No se crean tablas ni columnas nuevas; no se añaden env vars/secretos nuevos"} ], "issues": [], "notes": "Hardening futuro opcional: validar/descartar emails con header CRLF injection en readSmtpOptions antes de pasar a nodemailer; fuera del scope de F-152.", "timestamp": "2026-08-22T05:05:10Z" }