feat: POST /api/emails + route ephemeral key to RECOUP_API_KEY (#1815)

Merged
SW
4mo ago
+703-74

Implements recoupable/chat#1815 — Item 1: let the sandbox agent (and scheduled report tasks) deliver email. Fulfills the contract in recoupable/docs#251.

Merge order: docs#251 → this → skills (recoup-api skill docs).

What's here

1. POST /api/emails (the endpoint)

Sends an email to explicit recipients (to[]), account-scoped via validateAuthContext, reusing the same processAndSendEmail domain fn as the send_email MCP tool (DRY). Mirrors POST /api/notifications but takes a required to (notifications only emails the account's own address).

  • SRP: app/api/emails/route.ts → lib/emails/sendEmailHandler.ts → lib/emails/validateSendEmailBody.ts.
  • Flat response { success, message, id }; 400 (bad/empty to, missing subject), 401 (no auth), 502 (Resend failure).

2. buildRecoupExecEnv: route the ephemeral key to RECOUP_API_KEY (the actual auth fix)

The headless /api/chat/runs path plumbs an ephemeral recoup_sk_ API key through recoupAccessToken. The old code put it in RECOUP_ACCESS_TOKEN, which the recoup-api skill sends as Authorization: Bearer — and REST endpoints 401 an API key over Bearer (that's the JWT path). This is why the sandbox agent's recoup-api calls were failing. Now a recoup_sk_ token surfaces as RECOUP_API_KEY (skill sends x-api-key); Privy JWTs (interactive path) still go to RECOUP_ACCESS_TOKEN.

  • Confirmed by diagnostic run on prod: GET /api/accounts/id → x-api-key 200, Bearer 401.

Tests (TDD red→green)

  • validateSendEmailBody (8), sendEmailHandler (4), buildRecoupExecEnv (+3 new, prefix routing).
  • Affected suites green (231). My files tsc + eslint clean. (The unrelated tsc errors in lib/emails/inbound/admins Resend-type tests pre-exist on test — 11 of them — not introduced here.)

Preview verification to follow as a comment once the deployment is Ready.


Summary by cubic

Adds POST /api/emails to send emails to explicit recipients. Updates auth to accept ephemeral recoup_sk_ keys via Bearer or x-api-key, and aligns with docs: chat_id plus recipient limits until a payment method is on file (recoupable/chat#1815).

  • New Features

    • POST /api/emails: sends via Resend to to[]; account-scoped via validateAuthContext; reuses processAndSendEmail.
    • Contract: public chat_id (replaces room_id); without a payment method, to/cc must be the account’s own email (403). Supports to (required), subject (required), cc, text, html, headers, chat_id, account_id (org keys). Returns { success, message, id }; errors: 400, 401, 403, 502. Includes CORS preflight.
  • Refactors

    • Server-side token parsing: getAuthenticatedAccountId treats recoup_sk_ Bearer tokens as API keys via shared getAccountIdByApiKey. buildRecoupExecEnv now always injects RECOUP_ACCESS_TOKEN.
    • Recipient gate moved to validateSendEmailBody. New accountHasPaymentMethod helper; reused in ensureSongstatsPaymentMethod.