From 2af3c8c0a1378753773f7d012e0a24003586dc5f Mon Sep 17 00:00:00 2001 From: Acho Arnold Ewin Date: Sat, 18 Jul 2026 09:53:35 +0300 Subject: [PATCH 1/8] docs: design webhook payload formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 48f6d946-ae22-4440-b7a1-44e939419b11 --- ...webhook-email-payload-formatting-design.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-18-webhook-email-payload-formatting-design.md diff --git a/docs/superpowers/specs/2026-07-18-webhook-email-payload-formatting-design.md b/docs/superpowers/specs/2026-07-18-webhook-email-payload-formatting-design.md new file mode 100644 index 000000000..56e2e7e81 --- /dev/null +++ b/docs/superpowers/specs/2026-07-18-webhook-email-payload-formatting-design.md @@ -0,0 +1,116 @@ +# Webhook Email Payload Formatting + +- Date: 2026-07-18 +- Status: Approved (design) +- Scope: `api/` email rendering only. + +## Problem + +Failed-webhook notification emails currently display the event payload as an +unformatted dictionary value. Nested JSON is difficult to scan, and multiline +payloads lose the visual structure users need when diagnosing webhook failures. + +## Decisions + +- Format only the webhook email's **Event Payload** value. +- Leave SMS content, failure reasons, HTTP responses, and every other email + field unchanged. +- Pretty-print valid JSON with indentation. +- Render valid JSON in the HTML email with lightweight colors for keys, strings, + numbers, booleans, and `null`. +- Render non-JSON payloads in the same monospace block while preserving their + original whitespace, without syntax colors. +- Use only the Go standard library and the existing Hermes email integration. +- Keep the plain-text email readable with indentation but no HTML styling. + +## Design + +### 1. Payload formatter + +Add a focused formatter under `api/pkg/emails` that accepts the event payload +string and returns: + +- a plain-text value for `hermes.Entry.Value`; and +- safely escaped highlighted markup for `hermes.Entry.UnsafeValue`. + +For valid JSON, use the Go standard library to produce deterministic +indentation. A small JSON-aware tokenizer will wrap keys, strings, numbers, +booleans, and `null` in spans with inline colors. Inline styles are preferred +because many email clients strip style blocks or external styles. + +All payload tokens must be HTML-escaped before they are inserted into the +trusted template value. Payload content must never be interpreted as HTML. + +For invalid JSON, return the original payload unchanged as the plain-text value +and HTML-escape it into an unhighlighted code block. Invalid JSON is an expected +fallback and must not prevent email generation or delivery. + +### 2. Hermes dictionary rendering + +Update the custom HTML template in `api/pkg/emails/hermes_theme.go` so dictionary +entries render `UnsafeValue` when it is present and otherwise retain the current +escaped `Value` rendering. + +The payload block will use email-compatible markup and inline styles: + +- a neutral light background and subtle border; +- padding and a monospace font; +- `white-space: pre-wrap` to preserve indentation and line breaks; and +- safe wrapping for long values so the email remains usable on narrow clients. + +The plain-text template continues to render `Entry.Value`, so it contains +readable indented JSON without HTML tags or colors. + +### 3. Webhook email factory + +In `hermesNotificationEmailFactory.WebhookSendFailed`, pass +`payload.EventPayload` through the formatter. Set both `Value` and +`UnsafeValue` only on the existing `Event Payload` dictionary entry. + +No other dictionary entry or notification email uses the new unsafe rendering +path. The order, subject, explanatory text, action button, and delivery behavior +of the webhook failure email remain unchanged. + +## Data Flow + +1. `WebhookSendFailed` receives `payload.EventPayload`. +2. The formatter checks whether the string is valid JSON. +3. Valid JSON is indented and highlighted; invalid JSON preserves its original + text and receives code-block styling only. +4. Hermes uses the plain value for the text email and the escaped styled value + for the HTML email. +5. The existing mailer sends both representations without service or listener + changes. + +## Error Handling and Security + +- Invalid JSON falls back to a whitespace-preserving code block. +- Formatting does not introduce a new email-generation failure path. +- Payload markup, quotes, ampersands, and other special characters are escaped + before insertion. +- Highlighting is generated server-side and requires no JavaScript, remote + assets, or external CSS. +- Existing Hermes generation errors continue to use the current stacktrace + propagation behavior. + +## Testing + +Add targeted tests under `api/pkg/emails` covering: + +- nested valid JSON is consistently indented; +- keys, strings, numbers, booleans, and `null` receive the expected HTML spans; +- payload HTML is escaped and cannot inject markup; +- invalid JSON preserves whitespace and is not syntax-highlighted; +- the webhook email HTML renders only `Event Payload` as a code block; +- the webhook email plain text contains readable indented JSON; and +- existing non-payload dictionary entries keep their current rendering. + +Run the targeted email package tests, then `go test ./...` in `api/`. + +## Out of Scope + +- Formatting SMS message content or failure reasons. +- Formatting HTTP response bodies or error messages. +- Highlighting non-JSON languages. +- Adding a third-party syntax-highlighting dependency. +- Changing webhook retries, event payload generation, or email delivery logic. From ede8a3d7f4319a119090a8ed8138d60f3adaa609 Mon Sep 17 00:00:00 2001 From: Acho Arnold Ewin Date: Sat, 18 Jul 2026 10:34:15 +0300 Subject: [PATCH 2/8] docs: plan webhook payload formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 48f6d946-ae22-4440-b7a1-44e939419b11 --- ...-07-18-webhook-email-payload-formatting.md | 584 ++++++++++++++++++ 1 file changed, 584 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-18-webhook-email-payload-formatting.md diff --git a/docs/superpowers/plans/2026-07-18-webhook-email-payload-formatting.md b/docs/superpowers/plans/2026-07-18-webhook-email-payload-formatting.md new file mode 100644 index 000000000..1fe56a219 --- /dev/null +++ b/docs/superpowers/plans/2026-07-18-webhook-email-payload-formatting.md @@ -0,0 +1,584 @@ +# Webhook Email Payload Formatting Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Render the webhook failure email's Event Payload as readable indented JSON with safe lightweight syntax colors in HTML and readable indentation in plain text. + +**Architecture:** Add a focused formatter in `api/pkg/emails` that produces a plain string and a safely constructed `template.HTML` code block. Teach the custom Hermes HTML dictionary template to use `hermes.Entry.UnsafeValue` when explicitly supplied, then wire only the webhook email's Event Payload entry to that path. + +**Tech Stack:** Go 1.25.8 module, Go standard library (`bytes`, `encoding/json`, `html`, `html/template`, `strings`), go-hermes v2.6.2, testify v1.11.1. + +## Global Constraints + +- Format only the webhook email's **Event Payload** value. +- Leave SMS content, failure reasons, HTTP responses, and every other email field unchanged. +- Pretty-print valid JSON with indentation. +- Render valid JSON in HTML with lightweight colors for keys, strings, numbers, booleans, and `null`. +- Render invalid JSON in the same monospace block with original whitespace and no syntax colors. +- Add no third-party dependency. +- HTML-escape every payload token before converting the completed markup to `template.HTML`. +- Invalid JSON must not prevent email generation or delivery. +- Preserve the existing plain-text email behavior except for readable payload indentation. +- Use `stacktrace.Propagate` for existing Hermes generation errors; this feature introduces no new returned error. + +--- + +## File Structure + +- Create `api/pkg/emails/event_payload_formatter.go`: JSON indentation, token highlighting, HTML escaping, and code-block construction. +- Create `api/pkg/emails/event_payload_formatter_test.go`: focused tests for valid JSON tokens, escaping, and invalid JSON fallback. +- Create `api/pkg/emails/hermes_theme_test.go`: verifies safe opt-in HTML dictionary rendering and unchanged plain-text rendering. +- Modify `api/pkg/emails/hermes_theme.go:325-334`: render `Entry.UnsafeValue` only when present. +- Create `api/pkg/emails/hermes_notification_email_factory_test.go`: end-to-end factory coverage for the webhook notification email. +- Modify `api/pkg/emails/hermes_notification_email_factory.go:78-122`: format and attach only the Event Payload dictionary entry. + +### Task 1: Build the event payload formatter + +**Files:** +- Create: `api/pkg/emails/event_payload_formatter.go` +- Create: `api/pkg/emails/event_payload_formatter_test.go` + +**Interfaces:** +- Consumes: a raw webhook event payload `string`. +- Produces: `formatEventPayload(payload string) (string, template.HTML)`. +- Produces: plain indented JSON for `hermes.Entry.Value` and an escaped styled code block for `hermes.Entry.UnsafeValue`. + +- [ ] **Step 1: Write the failing formatter tests** + +Create `api/pkg/emails/event_payload_formatter_test.go`: + +```go +package emails + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormatEventPayloadIndentsAndHighlightsJSON(t *testing.T) { + payload := `{"message":"hello","count":2,"ratio":1.5,"enabled":true,"disabled":false,"missing":null,"nested":{"value":"ok"}}` + + plain, rich := formatEventPayload(payload) + html := string(rich) + + assert.Equal(t, `{ + "message": "hello", + "count": 2, + "ratio": 1.5, + "enabled": true, + "disabled": false, + "missing": null, + "nested": { + "value": "ok" + } +}`, plain) + assert.Contains(t, html, `"message"`) + assert.Contains(t, html, `"hello"`) + assert.Contains(t, html, `2`) + assert.Contains(t, html, `1.5`) + assert.Contains(t, html, `true`) + assert.Contains(t, html, `false`) + assert.Contains(t, html, `null`) + assert.Contains(t, html, `white-space:pre-wrap`) +} + +func TestFormatEventPayloadEscapesPayloadHTML(t *testing.T) { + plain, rich := formatEventPayload(`{"message":"&"}`) + html := string(rich) + + assert.Contains(t, plain, `&"}`) + html := string(rich) + + assert.Contains(t, plain, `