Skip to content

feat: add POST /api/notifications endpoint#241

Merged
sweetmantech merged 4 commits intotestfrom
sweetmantech/myc-4334-api-post-apinotifications-follow-the-new-docs-see-mcp-tool
Feb 25, 2026
Merged

feat: add POST /api/notifications endpoint#241
sweetmantech merged 4 commits intotestfrom
sweetmantech/myc-4334-api-post-apinotifications-follow-the-new-docs-see-mcp-tool

Conversation

@sweetmantech
Copy link
Copy Markdown
Contributor

@sweetmantech sweetmantech commented Feb 25, 2026

Summary

  • Add POST /api/notifications REST API endpoint for sending notification emails via Resend
  • Mirrors the send_email MCP tool functionality, enabling sandbox agents to send emails via HTTP
  • Supports to, cc, subject, text (Markdown), html, custom headers, and room_id for chat footer links
  • Uses validateAuthContext for auth (supports both API key and Bearer token)
  • Reuses existing email utilities (sendEmailWithResend, getEmailFooter, marked)

Test plan

  • 15 unit tests passing (handler + validation)
  • Manual test with API key: curl -X POST -H "x-api-key: <key>" -H "Content-Type: application/json" -d '{"to":["[email protected]"],"subject":"Test","text":"Hello"}' https://recoup-api.vercel.app/api/notifications

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a notifications API for sending emails with TO, CC, headers, subject, HTML or text bodies.
    • Requests are validated and CORS-enabled; supports authenticated requests.
    • Email processing now supports optional room-related content, markdown-to-HTML conversion, and appends a footer.
  • Refactor

    • Centralized email sending flow for consistent success/error reporting.

REST API endpoint for sending notification emails via Resend, mirroring
the send_email MCP tool. Supports to, cc, subject, text (Markdown),
html, custom headers, and room_id for chat footer links.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Feb 25, 2026

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

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Feb 25, 2026 2:27pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 25, 2026

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

Adds a new notifications endpoint and supporting server-side email pipeline: route handlers (OPTIONS/POST), request validation (Zod), authenticated notification creation, centralized email processing/sending, and adjustments to a tool to use the new processor. CORS and route cache controls are included.

Changes

Cohort / File(s) Summary
API Route
app/api/notifications/route.ts
Adds OPTIONS CORS preflight and POST handler that delegates to createNotificationHandler. Exports route config: dynamic="force-dynamic", fetchCache="force-no-store", revalidate=0.
Notification Handler
lib/notifications/createNotificationHandler.ts
New handler: validates auth, parses/validates body via validateCreateNotificationBody, resolves recipient email, calls processAndSendEmail, and returns 200/400/502 responses with CORS headers.
Request Validation
lib/notifications/validateCreateNotificationBody.ts
Adds Zod schema and validateCreateNotificationBody which returns typed data or a NextResponse 400 JSON error with CORS headers on validation failure.
Email Processing
lib/emails/processAndSendEmail.ts
New centralized email processing/sending module: resolves optional room data, builds HTML (markdown fallback), appends footer, calls Resend wrapper, and returns structured success/error results.
Tooling Update
lib/mcp/tools/registerSendEmailTool.ts
Replaces inline send logic with processAndSendEmail call; simplifies success/error mapping to tool result helpers and removes direct Resend/sendEmailWithResend usage.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Route as "app/api/notifications/route.ts"
    participant Handler as "createNotificationHandler"
    participant Auth as "validateAuthContext"
    participant Validator as "validateCreateNotificationBody"
    participant Processor as "processAndSendEmail"
    participant EmailAPI as "Resend / sendEmail wrapper"
    participant Response as Response

    Client->>Route: POST /api/notifications
    Route->>Handler: forward NextRequest
    Handler->>Auth: validateAuthContext(request)
    Auth-->>Handler: auth result
    Handler->>Validator: validateCreateNotificationBody(body)
    Validator-->>Handler: validated body / error
    Handler->>Processor: processAndSendEmail({to,cc,subject,text,html,headers,room_id})
    Processor->>EmailAPI: sendEmailWithResend(payload)
    EmailAPI-->>Processor: success / error
    Processor-->>Handler: success {id,message} / error
    Handler->>Response: NextResponse JSON (200 / 400 / 502) with CORS headers
    Response-->>Client: JSON payload
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

✉️ New routes awake with careful cheer,
Zod checks the shape, auth standing near.
Processor crafts body, footer in tow,
Resend flies messages onward to go —
Logs hum softly: deliveries clear. 🚀

🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Solid & Clean Code ✅ Passed PR successfully applies SOLID principles and clean code practices. DRY principle eliminated 21 lines of duplication between POST handler and MCP tool while maintaining SRP boundaries. Functions are appropriately sized (41-83 lines), use clear naming, include JSDoc, and maintain shallow nesting. Each module has focused responsibility without over-engineering.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sweetmantech/myc-4334-api-post-apinotifications-follow-the-new-docs-see-mcp-tool

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Remove 'to' param from request body. The recipient email is now
automatically resolved from the account_emails table using the
authenticated account's ID. Returns 400 if no email is found.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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 (1)
lib/notifications/createNotificationHandler.ts (1)

20-73: Split this handler into smaller units to meet the file-level function-size rule.

This function currently mixes auth, parsing, validation, enrichment, rendering, provider invocation, and response mapping in one block. Extracting a couple of helpers will improve readability and testability.

As per coding guidelines "lib/**/*.ts: ... Keep functions under 50 lines".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/notifications/createNotificationHandler.ts` around lines 20 - 73, The
createNotificationHandler function is too large and should be split into small
helpers: extract a parseAndValidateBody helper that calls safeParseJson and
validateCreateNotificationBody and returns the validated payload (or a
NextResponse on error); extract an enrichAndRenderEmail helper that accepts the
validated payload and uses selectRoomWithArtist, getEmailFooter, and marked to
return the final email fields (to, cc, subject, htmlWithFooter, headers,
room_id); and extract a mapSendResultToResponse helper that takes the
sendEmailWithResend result and RECOUP_FROM_EMAIL and returns the appropriate
NextResponse (including error mapping and getCorsHeaders). Then simplify
createNotificationHandler to orchestrate: validateAuthContext, call
parseAndValidateBody, call enrichAndRenderEmail, call sendEmailWithResend, and
call mapSendResultToResponse. Use the existing function names
(createNotificationHandler, validateAuthContext, safeParseJson,
validateCreateNotificationBody, selectRoomWithArtist, getEmailFooter, marked,
sendEmailWithResend, getCorsHeaders) to locate code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/notifications/createNotificationHandler.ts`:
- Around line 21-35: validateAuthContext() returns the authenticated context but
the code calls selectRoomWithArtist(room_id) without scoping to that context;
update createNotificationHandler to authorize the room_id against the
authenticated account/org from validateAuthContext() before using room metadata:
after getting authResult (the object returned by validateAuthContext), extract
the account/org id (e.g., authResult.accountId or authResult.orgId), then either
replace selectRoomWithArtist(room_id) with a scoped lookup (e.g.,
selectRoomWithArtistForAccount(room_id, accountId)) or fetch roomData and assert
roomData.account_id/org_id matches the authenticated id and return an
unauthorized response (or strip room metadata) if it does not; ensure you
reference validateAuthContext, selectRoomWithArtist and the room_id variable
when making this change.
- Around line 48-54: In createNotificationHandler, the error shape from the
provider is a string, so change the extraction from data?.error?.message to
prefer the string error (e.g., const reason = data?.error || data?.message ||
String(data)); then return NextResponse.json with error: reason || `Failed to
send email from ${RECOUP_FROM_EMAIL} to ${to.join(", ")}.` so the upstream
reason is preserved when result (the NextResponse) is returned.

In `@lib/notifications/validateCreateNotificationBody.ts`:
- Line 8: The Zod schema in validateCreateNotificationBody.ts uses the
deprecated option key `{ message: "..." }` on z.string — update the schema for
the `subject` field to use `{ error: "subject is required" }` instead of `{
message: "subject is required" }`, or simply remove that option and rely on
`z.string().min(1, "subject cannot be empty")`; locate the `subject:
z.string(...)` entry in the exported validation schema (e.g., the create
notification schema variable/function) and make this change so Zod v4 correctly
reports the error.

---

Nitpick comments:
In `@lib/notifications/createNotificationHandler.ts`:
- Around line 20-73: The createNotificationHandler function is too large and
should be split into small helpers: extract a parseAndValidateBody helper that
calls safeParseJson and validateCreateNotificationBody and returns the validated
payload (or a NextResponse on error); extract an enrichAndRenderEmail helper
that accepts the validated payload and uses selectRoomWithArtist,
getEmailFooter, and marked to return the final email fields (to, cc, subject,
htmlWithFooter, headers, room_id); and extract a mapSendResultToResponse helper
that takes the sendEmailWithResend result and RECOUP_FROM_EMAIL and returns the
appropriate NextResponse (including error mapping and getCorsHeaders). Then
simplify createNotificationHandler to orchestrate: validateAuthContext, call
parseAndValidateBody, call enrichAndRenderEmail, call sendEmailWithResend, and
call mapSendResultToResponse. Use the existing function names
(createNotificationHandler, validateAuthContext, safeParseJson,
validateCreateNotificationBody, selectRoomWithArtist, getEmailFooter, marked,
sendEmailWithResend, getCorsHeaders) to locate code.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25e4d3c and 6016a45.

⛔ Files ignored due to path filters (2)
  • lib/notifications/__tests__/createNotificationHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/notifications/__tests__/validateCreateNotificationBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (3)
  • app/api/notifications/route.ts
  • lib/notifications/createNotificationHandler.ts
  • lib/notifications/validateCreateNotificationBody.ts

Comment on lines +21 to +35
const authResult = await validateAuthContext(request);
if (authResult instanceof NextResponse) {
return authResult;
}

const body = await safeParseJson(request);
const validated = validateCreateNotificationBody(body);
if (validated instanceof NextResponse) {
return validated;
}

const { to, cc = [], subject, text, html = "", headers = {}, room_id } = validated;

const roomData = room_id ? await selectRoomWithArtist(room_id) : null;
const footer = getEmailFooter(room_id, roomData?.artist_name || undefined);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Authorize room_id against the authenticated account context before room lookup.

validateAuthContext() is called, but room_id is resolved via selectRoomWithArtist(room_id) without scoping it to the authenticated account/org. This allows authenticated callers to reference unrelated room IDs and enrich outbound emails with unauthorized workspace/artist metadata.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/notifications/createNotificationHandler.ts` around lines 21 - 35,
validateAuthContext() returns the authenticated context but the code calls
selectRoomWithArtist(room_id) without scoping to that context; update
createNotificationHandler to authorize the room_id against the authenticated
account/org from validateAuthContext() before using room metadata: after getting
authResult (the object returned by validateAuthContext), extract the account/org
id (e.g., authResult.accountId or authResult.orgId), then either replace
selectRoomWithArtist(room_id) with a scoped lookup (e.g.,
selectRoomWithArtistForAccount(room_id, accountId)) or fetch roomData and assert
roomData.account_id/org_id matches the authenticated id and return an
unauthorized response (or strip room metadata) if it does not; ensure you
reference validateAuthContext, selectRoomWithArtist and the room_id variable
when making this change.

Extract room lookup, footer generation, markdown rendering, and Resend
call into lib/emails/processAndSendEmail.ts. Both the POST /api/notifications
handler and the send_email MCP tool now call the same shared function.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
lib/emails/processAndSendEmail.ts (2)

38-70: Normalize unexpected exceptions to preserve the declared result contract.

Line 43/45/48 call external helpers that can still throw; without a try/catch, callers may get unhandled exceptions instead of a ProcessAndSendEmailError.

♻️ Suggested hardening
 export async function processAndSendEmail(
   input: ProcessAndSendEmailInput,
 ): Promise<ProcessAndSendEmailResult> {
-  const { to, cc = [], subject, text, html = "", headers = {}, room_id } = input;
-
-  const roomData = room_id ? await selectRoomWithArtist(room_id) : null;
-  const footer = getEmailFooter(room_id, roomData?.artist_name || undefined);
-  const bodyHtml = html || (text ? await marked(text) : "");
-  const htmlWithFooter = `${bodyHtml}\n\n${footer}`;
-
-  const result = await sendEmailWithResend({
-    from: RECOUP_FROM_EMAIL,
-    to,
-    cc: cc.length > 0 ? cc : undefined,
-    subject,
-    html: htmlWithFooter,
-    headers,
-  });
-
-  if (result instanceof NextResponse) {
-    const data = await result.json();
-    return {
-      success: false,
-      error: data?.error?.message || `Failed to send email from ${RECOUP_FROM_EMAIL} to ${to.join(", ")}.`,
-    };
-  }
-
-  return {
-    success: true,
-    message: `Email sent successfully from ${RECOUP_FROM_EMAIL} to ${to.join(", ")}. CC: ${cc.length > 0 ? cc.join(", ") : "none"}.`,
-    id: result.id,
-  };
+  try {
+    const { to, cc = [], subject, text, html = "", headers = {}, room_id } = input;
+
+    const roomData = room_id ? await selectRoomWithArtist(room_id) : null;
+    const footer = getEmailFooter(room_id, roomData?.artist_name || undefined);
+    const bodyHtml = html || (text ? await marked(text) : "");
+    const htmlWithFooter = `${bodyHtml}\n\n${footer}`;
+
+    const result = await sendEmailWithResend({
+      from: RECOUP_FROM_EMAIL,
+      to,
+      cc: cc.length > 0 ? cc : undefined,
+      subject,
+      html: htmlWithFooter,
+      headers,
+    });
+
+    if (result instanceof NextResponse) {
+      const data = await result.json();
+      return {
+        success: false,
+        error: data?.error?.message || "Failed to send email.",
+      };
+    }
+
+    return {
+      success: true,
+      message: "Email sent successfully.",
+      id: result.id,
+    };
+  } catch (error) {
+    return {
+      success: false,
+      error: error instanceof Error ? error.message : "Failed to process and send email.",
+    };
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/emails/processAndSendEmail.ts` around lines 38 - 70, Wrap the body of
processAndSendEmail in a try/catch to normalize any thrown exceptions from
selectRoomWithArtist, getEmailFooter, marked, or sendEmailWithResend and always
return a ProcessAndSendEmailResult; on error return success: false and set error
to a concise message (include the caught error.message when available) instead
of letting exceptions propagate, preserving the declared result contract and
keeping existing success path intact.

61-67: Avoid returning raw recipient addresses in public messages.

Line 61 and Line 67 include to/cc values in returned strings. Prefer generic response messages and keep address details out of API/tool payloads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/emails/processAndSendEmail.ts` around lines 61 - 67, The function
processAndSendEmail.ts currently embeds raw recipient addresses (variables to
and cc) and the sender (RECOUP_FROM_EMAIL) into the returned message object;
change the success and error message strings to avoid exposing email addresses
by replacing them with generic text (e.g., "Email sent successfully" or "Failed
to send email") and, if needed for internal diagnostics, log the full addresses
locally rather than returning them in the response object. Update the return
objects where message uses to.join(", ") and cc.join(", ") (and the error
message that references RECOUP_FROM_EMAIL and to) so they no longer include
identifiable email data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/emails/processAndSendEmail.ts`:
- Around line 38-70: Wrap the body of processAndSendEmail in a try/catch to
normalize any thrown exceptions from selectRoomWithArtist, getEmailFooter,
marked, or sendEmailWithResend and always return a ProcessAndSendEmailResult; on
error return success: false and set error to a concise message (include the
caught error.message when available) instead of letting exceptions propagate,
preserving the declared result contract and keeping existing success path
intact.
- Around line 61-67: The function processAndSendEmail.ts currently embeds raw
recipient addresses (variables to and cc) and the sender (RECOUP_FROM_EMAIL)
into the returned message object; change the success and error message strings
to avoid exposing email addresses by replacing them with generic text (e.g.,
"Email sent successfully" or "Failed to send email") and, if needed for internal
diagnostics, log the full addresses locally rather than returning them in the
response object. Update the return objects where message uses to.join(", ") and
cc.join(", ") (and the error message that references RECOUP_FROM_EMAIL and to)
so they no longer include identifiable email data.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6016a45 and 6dab098.

⛔ Files ignored due to path filters (4)
  • lib/emails/__tests__/processAndSendEmail.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/mcp/tools/__tests__/registerSendEmailTool.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/notifications/__tests__/createNotificationHandler.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/notifications/__tests__/validateCreateNotificationBody.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (5)
  • app/api/notifications/route.ts
  • lib/emails/processAndSendEmail.ts
  • lib/mcp/tools/registerSendEmailTool.ts
  • lib/notifications/createNotificationHandler.ts
  • lib/notifications/validateCreateNotificationBody.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/notifications/createNotificationHandler.ts
  • lib/notifications/validateCreateNotificationBody.ts

Next.js compiler doesn't narrow !result.success on the
ProcessAndSendEmailResult union. Use result.success === false
for correct type narrowing in both MCP tool and API handler.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@sweetmantech sweetmantech merged commit da00919 into test Feb 25, 2026
2 of 3 checks passed
@sweetmantech sweetmantech mentioned this pull request Feb 25, 2026
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