feat: POST /api/emails + route ephemeral key to RECOUP_API_KEY (#1815)
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/emptyto, 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-key200,Bearer401.
Tests (TDD red→green)
validateSendEmailBody(8),sendEmailHandler(4),buildRecoupExecEnv(+3 new, prefix routing).- Affected suites green (231). My files
tsc+eslintclean. (The unrelatedtscerrors inlib/emails/inbound/adminsResend-type tests pre-exist ontest— 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 toto[]; account-scoped viavalidateAuthContext; reusesprocessAndSendEmail. - Contract: public
chat_id(replacesroom_id); without a payment method,to/ccmust be the account’s own email (403). Supportsto(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.
- POST
-
Refactors
- Server-side token parsing:
getAuthenticatedAccountIdtreatsrecoup_sk_Bearer tokens as API keys via sharedgetAccountIdByApiKey.buildRecoupExecEnvnow always injectsRECOUP_ACCESS_TOKEN. - Recipient gate moved to
validateSendEmailBody. NewaccountHasPaymentMethodhelper; reused inensureSongstatsPaymentMethod.
- Server-side token parsing: