Skip to content

Email comment notifications to doc owner and thread participants#2

Open
rgarcia wants to merge 4 commits into
mainfrom
hypeship/comment-notify-emails
Open

Email comment notifications to doc owner and thread participants#2
rgarcia wants to merge 4 commits into
mainfrom
hypeship/comment-notify-emails

Conversation

@rgarcia

@rgarcia rgarcia commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Emails a notification when someone comments on a document, mirroring the existing share notification.

  • Recipients. A top-level comment notifies the document owner. A reply notifies the owner plus the thread's other participants, deduped by user and excluding the comment's author. Reply participants are derived from comment-authorship history, so each non-owner participant is filtered through a live access check (public, or an owner/email/domain grant) before notifying — a participant whose grant was revoked, or who commented while the doc was public before it went private, is dropped so post-revocation thread activity and other participants' emails don't leak. The owner is always notified.
  • Single-use login link. Each recipient gets their own single-use, 7-day share-kind login token landing on /d/:slug, built exactly like the share email's verify URL so /login/verify consumes it unchanged. If it expires, the /d/:slug "was this shared with you? sign in" fallback recovers.
  • Two layouts, both inside the locked man-page email style. Top-level (layout C) shows the anchored passage above the body quote when the comment is anchored and still resolves. Reply (layout D) shows minimal parent context (parent author + snippet) above the reply quote. The footer's "why am I getting this" line keys off owner vs participant.
  • Dedicated rate-limit namespace. Suppression uses a per-recipient cmt-notify:addr:<email> daily cap (30/day) — never EMAIL_SEND_LIMITS — so comment volume can't burn a recipient's login/share email budget.
  • Best-effort. The comment is already committed before the send runs; a tripped cap or send failure never fails the request. A send failure rolls back only that recipient's just-minted token row. The 201 response surfaces a notified count.

Wired into POST /api/v1/docs/:slug/comments after the comment commits; CommentCreatedResponse gains a notified integer (OpenAPI spec regenerated via gen:spec).

Test plan

  • npm test — 99 passed (6 files). Adds lib/docs/comment-notify.test.ts covering the recipient model (self-suppression, owner-only top-level, missing-owner drop, reply dedupe/author-exclusion), access-revocation drop, public-doc skip of the grants lookup, the dedicated cmt-notify cap with the EMAIL_SEND_LIMITS-isolation guard, token/idempotency shape, snippet truncation, anchored-quote and parent-context surfacing, send-failure rollback, and the audit event.
  • npm run spec:check — OK; committed spec artifacts in sync, 25 endpoints agree across the generated spec, route handlers, and /llms.txt.
  • npx tsc --noEmit — clean.

Note

Medium Risk
Touches email delivery, login token minting, and access-gated recipient selection on the comment write path; failures are best-effort but wrong recipient logic could leak thread activity or emails to revoked grantees.

Overview
Adds best-effort email notifications when a comment is created, wired into POST /api/v1/docs/:slug/comments after the comment commits. The 201 response now includes notified (count of emails actually sent); OpenAPI/CommentCreatedResponse updated accordingly.

Recipients: top-level comments notify the document owner only; replies notify the owner plus other thread participants (deduped, author excluded). Reply participants are filtered by current access (resolveAccess on private docs; re-read is_public so a public→private flip doesn’t leak). Owner always notified when applicable.

Delivery: new sendCommentNotification orchestrator mints per-recipient 7-day share-kind login tokens to /d/:slug (same verify flow as share emails), uses dedicated cmt-notify:addr:<email> rate cap (30/day, not EMAIL_SEND_LIMITS), rolls back token on send failure, audits comment_notification.sent. New sendCommentEmail in lib/auth/email.ts with top-level vs reply layouts (anchored quote / parent context).

Extensive unit tests in lib/docs/comment-notify.test.ts.

Reviewed by Cursor Bugbot for commit 550e8fa. Bugbot is set up for automated code reviews on this repo. Configure here.

rgarcia and others added 2 commits June 22, 2026 22:38
Sibling of the share notification. On a new comment, best-effort email the
people who should hear about it, each with their own single-use 7-day
share-kind login link to /d/:slug:
  - top-level comment (parent_id null): the document owner.
  - reply: the owner plus the thread's other participants, deduped.
  - always exclude the comment's author.

Two layouts inside the locked man-page email style: top-level shows the
anchored passage (when present) above the body; replies show minimal parent
context. The footer's "why am I getting this" line keys off owner vs
participant.

Suppression uses a dedicated cmt-notify:addr daily cap so comment volume
never burns the recipient's login/share email budget. The send is wired into
POST /comments after the comment commits and surfaces a notified count on the
201 response; a send failure rolls back only the just-minted token row and
never fails the request.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reply recipients were derived purely from comment-authorship history, so a
thread participant who had since lost access (grant revoked, or doc flipped
from public to private) kept receiving reply notifications — leaking thread
activity and other participants' email addresses to someone who can no longer
view the doc. Filter each non-owner participant through a live access check
(public, or an owner/email/domain grant) before notifying; the owner is always
notified. Also scope the reply parent-context lookup by doc_id and
deleted_at IS NULL to match the rest of the comments module.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@pulumi

pulumi Bot commented Jun 22, 2026

Copy link
Copy Markdown

🤖 Pulumi Neo didn't review this pull request: no Pulumi preview ran for it. Agentic reviews require a preview of the affected stacks (for example from your CI's pulumi preview).

@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
justhtml Ready Ready Preview, Comment Jun 22, 2026 11:20pm

@rgarcia rgarcia marked this pull request as ready for review June 22, 2026 23:07
@firetiger-agent

Copy link
Copy Markdown

Firetiger deploy monitoring skipped

This PR didn't match the auto-monitor filter configured on your GitHub connection:

PRs in the kernel, infra, hypeman, and hypeship repos. kernel is a ~mono repo with many logical services underneath, ensure to focus on the implicated service for the PR

Reason: PR lacks repository context; cannot confirm whether this affects the kernel, infra, hypeman, or hypeship repos or which service within kernel is implicated.

To monitor this PR anyway, reply with @firetiger monitor this.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Outer catch zeroes notified count
    • I moved notified/recipients counters outside the main try block so the outer catch now returns the accurate partial send count instead of resetting to zero.
  • ✅ Fixed: Stale public flag skips access filter
    • The comments POST flow now re-reads the document before sending notifications so recipient filtering uses current visibility when applying access checks.

Create PR

Or push these changes by commenting:

@cursor push e9e1ca2726
Preview (e9e1ca2726)
diff --git a/app/api/v1/docs/[slug]/comments/route.ts b/app/api/v1/docs/[slug]/comments/route.ts
--- a/app/api/v1/docs/[slug]/comments/route.ts
+++ b/app/api/v1/docs/[slug]/comments/route.ts
@@ -139,7 +139,8 @@
   // is already committed, so a send failure or tripped cap never fails the
   // request. Notifies the owner (top-level) or the owner + thread participants
   // (replies), minus the author.
-  const { notified } = await sendCommentNotification({ req, doc, comment: result.comment });
+  const notifyDoc = (await findBySlug(slug)) ?? doc;
+  const { notified } = await sendCommentNotification({ req, doc: notifyDoc, comment: result.comment });
 
   return json({ comment: commentView(result.comment, []), notified }, 201);
 }

diff --git a/lib/docs/comment-notify.ts b/lib/docs/comment-notify.ts
--- a/lib/docs/comment-notify.ts
+++ b/lib/docs/comment-notify.ts
@@ -127,10 +127,13 @@
   doc: DocRow;
   comment: CommentRow;
 }): Promise<CommentNotifyResult> {
+  let notified = 0;
+  let recipientsCount = 0;
   try {
     const { req, doc, comment } = opts;
     const recipients = await resolveRecipients(doc, comment);
-    if (recipients.length === 0) return { notified: 0, recipients: 0 };
+    recipientsCount = recipients.length;
+    if (recipientsCount === 0) return { notified: 0, recipients: 0 };
 
     const isReply = comment.parent_id !== null;
     const title = doc.title || doc.slug;
@@ -166,7 +169,6 @@
     const next = `/d/${encodeURIComponent(doc.slug)}`;
     const docUrl = `${ORIGIN}${next}`;
 
-    let notified = 0;
     for (const r of recipients) {
       const to = r.email.toLowerCase();
 
@@ -220,9 +222,9 @@
       notified += 1;
     }
 
-    return { notified, recipients: recipients.length };
+    return { notified, recipients: recipientsCount };
   } catch {
     // Best-effort: a comment notification must never fail the comment write.
-    return { notified: 0, recipients: 0 };
+    return { notified, recipients: recipientsCount };
   }
 }

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 954bf1c. Configure here.

Comment thread lib/docs/comment-notify.ts
Comment thread lib/docs/comment-notify.ts
The owner has an account, so the share-grantee recovery copy ("no account
needed" / "was this shared with you? sign in") is wrong for them. Branch the
footer so owners are told to sign in normally and participants keep the
grantee-oriented copy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread lib/docs/comment-notify.ts
Re-read the document's current is_public at notification time so a
public->private flip between the POST's initial doc load and the
notification can't skip the participant access gate (which would email
participants who no longer have access).

Isolate each recipient and keep the notified count outside the try block,
so a mid-loop failure (cap check or token mint) neither under-reports how
many emails were sent nor aborts the remaining recipients.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant