Skip to content

feat(notifications): validate template vars and clean up stale push subscriptions#162

Open
ochemegina-ai wants to merge 1 commit into
DelegoLabs:mainfrom
ochemegina-ai:feat/notifications-v2
Open

feat(notifications): validate template vars and clean up stale push subscriptions#162
ochemegina-ai wants to merge 1 commit into
DelegoLabs:mainfrom
ochemegina-ai:feat/notifications-v2

Conversation

@ochemegina-ai

@ochemegina-ai ochemegina-ai commented Jun 24, 2026

Copy link
Copy Markdown

Add structured TemplateRenderResult to prevent partial email sends when
required template variables are missing or rendering fails.

Add PushSubscriptionCleanupResult and cleanupPushSubscriptions() to remove
expired or repeatedly failing push subscriptions from the store. Thresholds
are configurable via PUSH_MAX_FAILURES and PUSH_STALE_DAYS env vars.

Add vitest and unit tests covering success and failure paths for both features.

Closes #136
Closes #137

Summary by CodeRabbit

  • New Features
    • Added templated email sending with variable rendering and structured error reporting.
    • Introduced push subscription tracking and cleanup, including handling for stale or repeatedly failing subscriptions.
  • Bug Fixes
    • Improved email sending resilience by returning send errors in a structured format instead of throwing.
    • Added validation to prevent sending emails with missing template data.

…ubscriptions

Add structured TemplateRenderResult to prevent partial email sends when
required template variables are missing or rendering fails.

Add PushSubscriptionCleanupResult and cleanupPushSubscriptions() to remove
expired or repeatedly failing push subscriptions from the store. Thresholds
are configurable via PUSH_MAX_FAILURES and PUSH_STALE_DAYS env vars.

Add vitest and unit tests covering success and failure paths for both features.

Closes DelegoLabs#136
Closes DelegoLabs#137
@drips-wave

drips-wave Bot commented Jun 24, 2026

Copy link
Copy Markdown

@ochemegina-ai Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Notification emails now render named templates into HTML/text results with structured render errors, and push subscriptions now live in an in-memory store with failure/age-based cleanup and delivery tracking. Vitest coverage was added for both notification paths.

Changes

Email template rendering

Layer / File(s) Summary
Template renderer
apps/backend/notifications/templates/index.ts
TemplateRenderResult is defined, required variables are checked, template HTML is loaded and interpolated, plain text is derived, and missing-template, missing-variable, and render errors return structured codes.
Templated email send
apps/backend/notifications/email/index.ts
EmailMessage.body is documented as pre-rendered content, TemplateRenderResult is re-exported, sendEmail remains an unimplemented stub, and sendTemplatedEmail renders templates before wrapping sender failures as structured errors.
Template tests and wiring
apps/backend/notifications/tests/templates.test.ts, apps/backend/notifications/package.json, apps/backend/notifications/tsconfig.json
Vitest is enabled for the notifications package, the module tsconfig includes email, push, template, and test files, and renderTemplate tests cover successful rendering, missing variables, empty strings, and unknown templates.

Push subscription cleanup

Layer / File(s) Summary
Push model and store
apps/backend/notifications/push/index.ts
PushSubscription and PushSubscriptionCleanupResult are added, along with an in-memory Map store and environment-driven helpers for failure and staleness thresholds.
Delivery tracking and cleanup
apps/backend/notifications/push/index.ts
The push module now exports add/get, delivery success and failure tracking, cleanup, list, clear, and a sendPush stub; cleanup removes subscriptions by failure count or age and reports scanned, removed, and failed counts.
Push cleanup tests
apps/backend/notifications/tests/push-cleanup.test.ts
Vitest coverage exercises retention and removal boundaries, environment overrides, delivery recovery, and store listing behavior.

Sequence Diagram(s)

sequenceDiagram
  participant sendTemplatedEmail
  participant renderTemplate
  participant sendEmail
  sendTemplatedEmail->>renderTemplate: templateName, vars
  renderTemplate-->>sendTemplatedEmail: TemplateRenderResult
  alt render error
    sendTemplatedEmail-->>sendTemplatedEmail: return structured error
  else render success
    sendTemplatedEmail->>sendEmail: EmailMessage.body
    sendEmail-->>sendTemplatedEmail: resolve or throw
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A bunny hopped through templates bright,
With text and HTML in sight 🐇
Then stale pushes went poof from the burrow,
And Vitest said, “Hooray for tomorrow!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The core behaviors for #136 and #137 appear implemented, but documentation of the new push cleanup environment variables cannot be verified from the summary. Add or confirm documentation for PUSH_MAX_FAILURES and PUSH_STALE_DAYS near the push module or contract, then recheck compliance.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely matches the two main notification changes: template-variable validation and push subscription cleanup.
Out of Scope Changes check ✅ Passed All listed changes stay within notifications email, templates, push, tests, package, and tsconfig, matching the stated objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (defensive_cruft). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@ochemegina-ai ochemegina-ai changed the title feat(notifications): validate template vars and clean up stale push s… feat(notifications): validate template vars and clean up stale push subscriptions Jun 24, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
apps/backend/notifications/tsconfig.json (1)

13-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

tests/**/* is included in the tsc build output.

Since build runs plain tsc with outDir: ./dist, including tests/**/* will emit compiled test files into the production bundle. Type-checking tests is desirable, but they shouldn't ship. Consider excluding tests from the build (e.g. exclude: ["tests/**/*"] here, with a separate tsconfig for typecheck/Vitest).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/notifications/tsconfig.json` around lines 13 - 19, The tsconfig
for notifications is currently including tests in the main TypeScript build,
which causes compiled test files to be emitted into the production bundle.
Update the configuration around the include list to keep production sources
under build output while removing tests from the emit path, and if tests still
need type-checking, point them to a separate config for test/Vitest usage. Use
the existing notifications tsconfig include settings and the build setup that
runs plain tsc with outDir to locate the change.
apps/backend/notifications/email/index.ts (1)

51-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Plain-text body is discarded.

renderTemplate produces both html and text, but EmailMessage only carries a single body and sendTemplatedEmail drops rendered.text. Once SMTP is wired, a multipart (HTML + text alternative) is generally preferred for deliverability. Consider extending EmailMessage to carry both so the text part isn't lost here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/notifications/email/index.ts` around lines 51 - 76, The
plain-text alternative is being dropped in sendTemplatedEmail, so update the
email flow to preserve both renderTemplate outputs. Extend EmailMessage and the
sendEmail/sendTemplatedEmail path to accept both html and text (or equivalent
multipart fields), and use the rendered text instead of collapsing everything
into a single body in sendTemplatedEmail. Keep the change centered around
sendTemplatedEmail, EmailMessage, and sendEmail so the multipart payload can be
passed through cleanly.
apps/backend/notifications/tests/push-cleanup.test.ts (1)

147-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Staleness "exactly at boundary" test passes only by timing luck.

daysAgo(30) is computed at registration, while staleCutoff is computed a few ms later inside cleanupPushSubscriptions, so new Date(lastActiveAt) < staleCutoff is true purely because of the elapsed wall-clock time between the two calls — not because the boundary itself is inclusive. The "exactly the boundary" intent isn't deterministically tested. Consider freezing time with vi.useFakeTimers()/vi.setSystemTime() (you already import vi) so the boundary semantics are asserted deterministically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/notifications/tests/push-cleanup.test.ts` around lines 147 -
157, The “exactly at boundary” push cleanup test is timing-dependent because
daysAgo(30) and cleanupPushSubscriptions() use slightly different wall-clock
instants. Update the push-cleanup test in push-cleanup.test.ts to freeze time
with vi.useFakeTimers()/vi.setSystemTime() around addPushSubscription,
daysAgo(30), and cleanupPushSubscriptions so the staleness boundary is asserted
deterministically rather than by millisecond drift.
apps/backend/notifications/push/index.ts (1)

131-148: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

failed counter is unreachable with the in-memory Map.

Map.prototype.delete does not throw, so the catch on lines 141-143 never runs and failed is always 0. The result field is therefore meaningless until the real DB/Redis layer lands, and the failed: 0 assertions in the tests (e.g. lines 122, 190) don't actually exercise a failure path. This is fine as forward-looking scaffolding for the persisted implementation, but consider a brief note so it isn't mistaken for live error tracking.

📝 Optional clarifying comment
       if (isFailureExceeded || isStale) {
+        // NOTE: Map.delete never throws; `failed` only becomes reachable once
+        // this is backed by a DB/Redis layer where deletion can error.
         try {
           subscriptions.delete(endpoint);
           removed += 1;
         } catch {
           failed += 1;
         }
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/notifications/push/index.ts` around lines 131 - 148, The
`failed` counter in the subscription cleanup logic is currently unreachable
because `subscriptions` is an in-memory `Map` and `Map.prototype.delete` does
not throw. In `removeStaleSubscriptions`, either add a brief code comment or
adjust the implementation to make it clear this `failed` path is scaffolding for
the future persisted store, not live error tracking. Keep the `removed`/`failed`
return shape in place for now, but make sure the intent is explicit near the
`for (const [endpoint, sub] of subscriptions)` loop and the
`subscriptions.delete(endpoint)` call so it is not mistaken for real failure
handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/backend/notifications/push/index.ts`:
- Around line 101-110: The cleanup cutoff documentation in recordDeliveryFailure
and the surrounding PUSH_MAX_FAILURES comments is off by one compared to the
actual behavior in cleanupPushSubscriptions, which removes subscriptions when
failureCount >= maxFailures. Update the wording near recordDeliveryFailure and
the threshold description to match the intended inclusive cutoff, using the
existing symbols cleanupPushSubscriptions, recordDeliveryFailure, and
PUSH_MAX_FAILURES so the semantics are unambiguous.

In `@apps/backend/notifications/templates/index.ts`:
- Around line 42-46: The interpolate helper is inserting vars[key] directly into
the HTML template, which allows HTML injection in rendered emails. Update
interpolate in templates/index.ts to escape substituted values before
replacement, while keeping the plain-text derivation path unchanged and
unescaped. Use the existing interpolate function and its callers to ensure only
HTML output is escaped, not the raw values used elsewhere.
- Line 12: The template loader in index.ts relies on readFile(join(__dirname,
`${templateName}.html`)), but the HTML templates are not present in the compiled
output, causing TEMPLATE_NOT_FOUND at runtime. Fix this by ensuring the
templates are copied or bundled alongside the built JavaScript for the
notification templates module, and keep the loading logic in
createNotificationTemplate/getTemplate compatible with the emitted
dist/templates layout.

---

Nitpick comments:
In `@apps/backend/notifications/email/index.ts`:
- Around line 51-76: The plain-text alternative is being dropped in
sendTemplatedEmail, so update the email flow to preserve both renderTemplate
outputs. Extend EmailMessage and the sendEmail/sendTemplatedEmail path to accept
both html and text (or equivalent multipart fields), and use the rendered text
instead of collapsing everything into a single body in sendTemplatedEmail. Keep
the change centered around sendTemplatedEmail, EmailMessage, and sendEmail so
the multipart payload can be passed through cleanly.

In `@apps/backend/notifications/push/index.ts`:
- Around line 131-148: The `failed` counter in the subscription cleanup logic is
currently unreachable because `subscriptions` is an in-memory `Map` and
`Map.prototype.delete` does not throw. In `removeStaleSubscriptions`, either add
a brief code comment or adjust the implementation to make it clear this `failed`
path is scaffolding for the future persisted store, not live error tracking.
Keep the `removed`/`failed` return shape in place for now, but make sure the
intent is explicit near the `for (const [endpoint, sub] of subscriptions)` loop
and the `subscriptions.delete(endpoint)` call so it is not mistaken for real
failure handling.

In `@apps/backend/notifications/tests/push-cleanup.test.ts`:
- Around line 147-157: The “exactly at boundary” push cleanup test is
timing-dependent because daysAgo(30) and cleanupPushSubscriptions() use slightly
different wall-clock instants. Update the push-cleanup test in
push-cleanup.test.ts to freeze time with vi.useFakeTimers()/vi.setSystemTime()
around addPushSubscription, daysAgo(30), and cleanupPushSubscriptions so the
staleness boundary is asserted deterministically rather than by millisecond
drift.

In `@apps/backend/notifications/tsconfig.json`:
- Around line 13-19: The tsconfig for notifications is currently including tests
in the main TypeScript build, which causes compiled test files to be emitted
into the production bundle. Update the configuration around the include list to
keep production sources under build output while removing tests from the emit
path, and if tests still need type-checking, point them to a separate config for
test/Vitest usage. Use the existing notifications tsconfig include settings and
the build setup that runs plain tsc with outDir to locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 90f5fe71-80ed-4f87-a1c6-c74fcb741e19

📥 Commits

Reviewing files that changed from the base of the PR and between b3d63c6 and 563612f.

📒 Files selected for processing (7)
  • apps/backend/notifications/email/index.ts
  • apps/backend/notifications/package.json
  • apps/backend/notifications/push/index.ts
  • apps/backend/notifications/templates/index.ts
  • apps/backend/notifications/tests/push-cleanup.test.ts
  • apps/backend/notifications/tests/templates.test.ts
  • apps/backend/notifications/tsconfig.json

Comment on lines +101 to +110
/**
* Increment the failure count for a subscription.
* The subscription will be eligible for cleanup once the count exceeds the
* PUSH_MAX_FAILURES threshold.
*/
export function recordDeliveryFailure(endpoint: string): void {
const sub = subscriptions.get(endpoint);
if (!sub) return;
sub.failureCount += 1;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Doc/threshold semantics mismatch: "exceeds" vs >=.

The comment says cleanup happens "once the count exceeds the PUSH_MAX_FAILURES threshold" (line 9 mirrors this with "after this many … failures"), but cleanupPushSubscriptions uses sub.failureCount >= maxFailures (line 134). With the default of 5, a subscription is removed at exactly 5 failures, not 6. The test on line 116 (failureCount: 5 → removed) confirms the >= behavior is intended, so the wording in lines 9 and 103-104 is the part that's off-by-one. Align the docs (or the operator) to avoid confusion about the actual cutoff.

📝 Suggested doc wording fix
 /**
  * Increment the failure count for a subscription.
- * The subscription will be eligible for cleanup once the count exceeds the
- * PUSH_MAX_FAILURES threshold.
+ * The subscription becomes eligible for cleanup once the count reaches the
+ * PUSH_MAX_FAILURES threshold (failureCount >= PUSH_MAX_FAILURES).
  */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Increment the failure count for a subscription.
* The subscription will be eligible for cleanup once the count exceeds the
* PUSH_MAX_FAILURES threshold.
*/
export function recordDeliveryFailure(endpoint: string): void {
const sub = subscriptions.get(endpoint);
if (!sub) return;
sub.failureCount += 1;
}
/**
* Increment the failure count for a subscription.
* The subscription becomes eligible for cleanup once the count reaches the
* PUSH_MAX_FAILURES threshold (failureCount >= PUSH_MAX_FAILURES).
*/
export function recordDeliveryFailure(endpoint: string): void {
const sub = subscriptions.get(endpoint);
if (!sub) return;
sub.failureCount += 1;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/notifications/push/index.ts` around lines 101 - 110, The cleanup
cutoff documentation in recordDeliveryFailure and the surrounding
PUSH_MAX_FAILURES comments is off by one compared to the actual behavior in
cleanupPushSubscriptions, which removes subscriptions when failureCount >=
maxFailures. Update the wording near recordDeliveryFailure and the threshold
description to match the intended inclusive cutoff, using the existing symbols
cleanupPushSubscriptions, recordDeliveryFailure, and PUSH_MAX_FAILURES so the
semantics are unambiguous.

import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -e html . apps/backend/notifications
cat apps/backend/notifications/package.json
fd -t f -e json -g '*.json' apps/backend/notifications | xargs -I{} sh -c 'echo "== {} =="; cat {}'

Repository: DelegoLabs/Delego

Length of output: 2155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== apps/backend/notifications/templates/index.ts =="
cat -n apps/backend/notifications/templates/index.ts

echo
echo "== asset-copy/build references in apps/backend/notifications =="
rg -n "copy|asset|html|templates" apps/backend/notifications/package.json apps/backend/notifications/tsconfig.json apps/backend/notifications -g '!**/dist/**' -g '!**/node_modules/**'

Repository: DelegoLabs/Delego

Length of output: 7927


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== apps/backend/notifications/templates/index.ts =="
cat -n apps/backend/notifications/templates/index.ts

echo
echo "== asset-copy/build references in apps/backend/notifications =="
rg -n "copy|asset|html|templates" apps/backend/notifications/package.json apps/backend/notifications/tsconfig.json apps/backend/notifications -g '!**/dist/**' -g '!**/node_modules/**'

Repository: DelegoLabs/Delego

Length of output: 7927


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== root package.json scripts =="
cat package.json | sed -n '1,220p'

echo
echo "== build/asset-copy references across repo =="
rg -n "\"build\"|copy.*html|copy.*assets|assets|templates/|approval-request\.html|notifications" . \
  -g '!**/dist/**' -g '!**/node_modules/**' -g '!**/.git/**'

Repository: DelegoLabs/Delego

Length of output: 7403


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== root package.json scripts =="
cat package.json | sed -n '1,220p'

echo
echo "== build/asset-copy references across repo =="
rg -n "\"build\"|copy.*html|copy.*assets|assets|templates/|approval-request\.html|notifications" . \
  -g '!**/dist/**' -g '!**/node_modules/**' -g '!**/.git/**'

Repository: DelegoLabs/Delego

Length of output: 7403


Copy notification templates during build. tsc emits dist/templates/*.js, but it does not copy templates/*.html, so readFile(join(__dirname, \${templateName}.html`))will fail in a built deployment and returnTEMPLATE_NOT_FOUND`. Add an asset-copy step or bundle the templates with the compiled output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/notifications/templates/index.ts` at line 12, The template
loader in index.ts relies on readFile(join(__dirname, `${templateName}.html`)),
but the HTML templates are not present in the compiled output, causing
TEMPLATE_NOT_FOUND at runtime. Fix this by ensuring the templates are copied or
bundled alongside the built JavaScript for the notification templates module,
and keep the loading logic in createNotificationTemplate/getTemplate compatible
with the emitted dist/templates layout.

Comment on lines +42 to +46
function interpolate(template: string, vars: Record<string, string>): string {
return template.replace(/\{\{(\w+)\}\}/g, (match, key: string) =>
Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Variable values are interpolated into HTML without escaping (HTML injection).

interpolate substitutes vars[key] directly into the HTML body. If any of these values (orderId, amount, approvalUrl, etc.) is derived from user-controlled or external data, the rendered email can contain injected markup/attributes (e.g. a crafted approvalUrl breaking out of an href, or arbitrary tags in amount). Escape values before substitution into HTML; the plain-text derivation should use the unescaped value.

🛡️ Proposed fix
+function escapeHtml(value: string): string {
+  return value
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;")
+    .replace(/'/g, "&`#39`;");
+}
+
 function interpolate(template: string, vars: Record<string, string>): string {
   return template.replace(/\{\{(\w+)\}\}/g, (match, key: string) =>
-    Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match
+    Object.prototype.hasOwnProperty.call(vars, key) ? escapeHtml(vars[key]) : match
   );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function interpolate(template: string, vars: Record<string, string>): string {
return template.replace(/\{\{(\w+)\}\}/g, (match, key: string) =>
Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match
);
}
function escapeHtml(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&`#39`;");
}
function interpolate(template: string, vars: Record<string, string>): string {
return template.replace(/\{\{(\w+)\}\}/g, (match, key: string) =>
Object.prototype.hasOwnProperty.call(vars, key) ? escapeHtml(vars[key]) : match
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/notifications/templates/index.ts` around lines 42 - 46, The
interpolate helper is inserting vars[key] directly into the HTML template, which
allows HTML injection in rendered emails. Update interpolate in
templates/index.ts to escape substituted values before replacement, while
keeping the plain-text derivation path unchanged and unescaped. Use the existing
interpolate function and its callers to ensure only HTML output is escaped, not
the raw values used elsewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Notifications] Add Push Subscription Cleanup Job [Notifications] Add Template Render Error Handling

1 participant