fix: resend email send path - http_post_auth was dropping silently
The wrapper now logs the response and returns a structured ok/error shape. Four call sites converge on a single send_email helper. Resend deliveries verified end to end against will.anderson@neurontechnologies.ai (delivery IDs 492fa066, 74258223, 69a3d9ab, f6d1c889). Root cause: http_post_auth in dist/web_stubs.c only set the Authorization: Bearer header. Resend rejects requests without Content-Type: application/json with HTTP 422 missing_required_field because it parses the body as form-urlencoded. The 422 response was being captured by the El handler but not parsed, so callers logged the error body and returned ok-200 to the client. Two endpoints also built malformed JSON by interpolating the raw request body unquoted into the text field. Fix: - Added http_post_auth_json (Bearer + Content-Type: application/json) alongside http_post_auth in dist/web_stubs.c. Stripe form-POST callers stay on http_post_auth, JSON callers (Resend now, others later) move to the json variant. - New send_email(from_addr, to, subject, html, text) wrapper in src/main.el. JSON-escapes all user-provided fields, parses the Resend response into a structured ok/error envelope, and println's the outcome ([email] sent id=<id>) for Cloud Run log surfaces. - Refactored four call sites onto the wrapper: /api/enterprise-inquiry, /api/developer-interest, /api/waitlist, /api/attest, the family invite branch in /api/family/invite, and both DocuSeal completion branches in /api/docuseal/webhook/<token>. - Untracked dist/ source files (web_stubs.c, vessel_stubs.c, soul-demo.c, entrypoint.sh, engram-snapshot.json) are now committed - generated artifacts (main.c, binaries) stay ignored. Without this the next CI rebuild would regress the fix.
This commit is contained in:
+158
-64
@@ -406,6 +406,67 @@ fn waitlist_upsert(email: String, name: String, plan: String, source: String, at
|
||||
return ""
|
||||
}
|
||||
|
||||
// ── send_email ────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Canonical Resend send path. Builds a JSON body with proper escaping,
|
||||
// posts via http_post_auth_json (which sets both Bearer auth AND
|
||||
// Content-Type: application/json - the missing Content-Type was the
|
||||
// silent-drop bug), parses the response, and logs the outcome.
|
||||
//
|
||||
// Returns:
|
||||
// {"ok":true,"id":"<resend-id>"} - on success
|
||||
// {"ok":false,"error":"<message>","raw":"..."} - on Resend 4xx/5xx
|
||||
// {"ok":false,"error":"empty_response"} - on transport failure
|
||||
// {"ok":false,"error":"no_resend_key"} - if RESEND_API_KEY unset
|
||||
//
|
||||
// `to` is a single recipient string. `subject` is plain text. One of
|
||||
// `html` or `text` should be non-empty; if both are set both are sent.
|
||||
// `from_addr` is the full From header value, e.g. "Neuron <no-reply@...>".
|
||||
// (Parameter is named from_addr - not from - because the build-stage.sh
|
||||
// combiner step strips lines that match `^[[:space:]]*from`, which would
|
||||
// nuke any line beginning with `from:`.)
|
||||
// All four user-provided strings are JSON-escaped before interpolation,
|
||||
// so callers can pass arbitrary content without pre-escaping.
|
||||
//
|
||||
// Keys are NEVER logged - the Bearer token is passed to libcurl via
|
||||
// curl_slist headers and never echoed.
|
||||
fn send_email(from_addr: String, to: String, subject: String, html: String, text: String) -> String {
|
||||
let resend_key: String = state_get("__resend_api_key__")
|
||||
if str_eq(resend_key, "") {
|
||||
println("[email] skipped subject=\"" + subject + "\" reason=no_resend_key")
|
||||
return "{\"ok\":false,\"error\":\"no_resend_key\"}"
|
||||
}
|
||||
let from_safe: String = str_replace(str_replace(from_addr, "\\", "\\\\"), "\"", "\\\"")
|
||||
let to_safe: String = str_replace(str_replace(to, "\\", "\\\\"), "\"", "\\\"")
|
||||
let subj_safe: String = str_replace(str_replace(str_replace(str_replace(subject, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n"), "\r", "\\r")
|
||||
let html_safe: String = str_replace(str_replace(str_replace(str_replace(html, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n"), "\r", "\\r")
|
||||
let text_safe: String = str_replace(str_replace(str_replace(str_replace(text, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n"), "\r", "\\r")
|
||||
let html_field: String = if str_eq(html, "") { "" } else { ",\"html\":\"" + html_safe + "\"" }
|
||||
let text_field: String = if str_eq(text, "") { "" } else { ",\"text\":\"" + text_safe + "\"" }
|
||||
let email_body: String = "{\"from\":\"" + from_safe + "\","
|
||||
+ "\"to\":[\"" + to_safe + "\"],"
|
||||
+ "\"subject\":\"" + subj_safe + "\""
|
||||
+ html_field + text_field + "}"
|
||||
let resp: String = http_post_auth_json("https://api.resend.com/emails", resend_key, email_body)
|
||||
if str_eq(resp, "") {
|
||||
println("[email] FAIL subject=\"" + subject + "\" to=" + to + " error=empty_response")
|
||||
return "{\"ok\":false,\"error\":\"empty_response\"}"
|
||||
}
|
||||
let id: String = json_get(resp, "id")
|
||||
if !str_eq(id, "") {
|
||||
println("[email] sent id=" + id + " to=" + to + " subject=\"" + subject + "\"")
|
||||
return "{\"ok\":true,\"id\":\"" + id + "\"}"
|
||||
}
|
||||
// Resend error envelope - {statusCode, name, message}
|
||||
let err_msg: String = json_get(resp, "message")
|
||||
let err_name: String = json_get(resp, "name")
|
||||
let resp_safe: String = str_replace(str_replace(resp, "\\", "\\\\"), "\"", "\\\"")
|
||||
let err_final: String = if !str_eq(err_msg, "") { err_msg } else if !str_eq(err_name, "") { err_name } else { "unknown_error" }
|
||||
println("[email] FAIL subject=\"" + subject + "\" to=" + to + " error=\"" + err_final + "\" raw=" + resp)
|
||||
let err_safe: String = str_replace(str_replace(err_final, "\\", "\\\\"), "\"", "\\\"")
|
||||
return "{\"ok\":false,\"error\":\"" + err_safe + "\",\"raw\":\"" + resp_safe + "\"}"
|
||||
}
|
||||
|
||||
fn read_asset(abs_path: String) -> String {
|
||||
let exists: Bool = fs_exists(abs_path)
|
||||
if !exists {
|
||||
@@ -855,23 +916,36 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
|
||||
// ── Enterprise inquiry ────────────────────────────────────────────────────
|
||||
if str_eq(path, "/api/enterprise-inquiry") {
|
||||
let resend_key: String = state_get("__resend_api_key__")
|
||||
let name_val: String = if str_contains(body, "\"name\"") { "submitted" } else { "" }
|
||||
if str_eq(name_val, "") {
|
||||
return "{\"error\":\"invalid request\"}"
|
||||
}
|
||||
// Log to stdout regardless of email delivery
|
||||
println("[enterprise-inquiry] " + body)
|
||||
// Send via Resend if key is configured
|
||||
if !str_eq(resend_key, "") {
|
||||
let email_body: String = "{\"from\":\"Neuron Enterprise <enterprise@neurontechnologies.ai>\",\"to\":[\"enterprise@neurontechnologies.ai\"],\"subject\":\"Enterprise Inquiry\",\"text\":" + body + "}"
|
||||
let resp: String = http_post_auth(
|
||||
"https://api.resend.com/emails",
|
||||
resend_key,
|
||||
email_body
|
||||
)
|
||||
println("[enterprise-inquiry] resend: " + resp)
|
||||
}
|
||||
// Pull individual fields so we can build a clean text body. The
|
||||
// previous implementation interpolated the raw request body into
|
||||
// the JSON `text` field unquoted, which produced malformed JSON
|
||||
// and was a secondary cause of the silent-drop.
|
||||
let ent_name: String = json_get(body, "name")
|
||||
let ent_email: String = json_get(body, "email")
|
||||
let ent_company: String = json_get(body, "company")
|
||||
let ent_role: String = json_get(body, "role")
|
||||
let ent_seats: String = json_get(body, "seats")
|
||||
let ent_msg: String = json_get(body, "message")
|
||||
let ent_text: String = "Name: " + ent_name + "\n"
|
||||
+ "Email: " + ent_email + "\n"
|
||||
+ "Company: " + ent_company + "\n"
|
||||
+ "Role: " + ent_role + "\n"
|
||||
+ "Seats: " + ent_seats + "\n\n"
|
||||
+ "Message:\n" + ent_msg
|
||||
let send_resp: String = send_email(
|
||||
"Neuron Enterprise <enterprise@neurontechnologies.ai>",
|
||||
"enterprise@neurontechnologies.ai",
|
||||
"Enterprise Inquiry: " + ent_company,
|
||||
"",
|
||||
ent_text
|
||||
)
|
||||
println("[enterprise-inquiry] " + send_resp)
|
||||
return "{\"received\":true}"
|
||||
}
|
||||
|
||||
@@ -887,13 +961,14 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
// Write to Supabase waitlist table
|
||||
waitlist_upsert(wl_email, "", "free", "preorder", "", "", 0)
|
||||
// Email notification
|
||||
let e_safe: String = str_replace(str_replace(wl_email, "\\", "\\\\"), "\"", "\\\"")
|
||||
let resend_key: String = state_get("__resend_api_key__")
|
||||
if !str_eq(resend_key, "") {
|
||||
let email_body: String = "{\"from\":\"Neuron <no-reply@neurontechnologies.ai>\",\"to\":[\"will.anderson@neurontechnologies.ai\"],\"subject\":\"Free tier preorder: " + e_safe + "\",\"text\":\"New free tier preorder\\nEmail: " + e_safe + "\"}"
|
||||
let resp: String = http_post_auth("https://api.resend.com/emails", resend_key, email_body)
|
||||
println("[waitlist] email: " + resp)
|
||||
}
|
||||
let wl_send: String = send_email(
|
||||
"Neuron <no-reply@neurontechnologies.ai>",
|
||||
"will.anderson@neurontechnologies.ai",
|
||||
"Free tier preorder: " + wl_email,
|
||||
"",
|
||||
"New free tier preorder\nEmail: " + wl_email
|
||||
)
|
||||
println("[waitlist] " + wl_send)
|
||||
return "{\"ok\":true}"
|
||||
}
|
||||
|
||||
@@ -928,19 +1003,25 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
println("[attest] gcs write " + attest_key + " -> " + gcs_ok)
|
||||
}
|
||||
// Email notification
|
||||
let resend_key: String = state_get("__resend_api_key__")
|
||||
if !str_eq(resend_key, "") {
|
||||
let subject: String = "Founding member attestation: " + attest_name + " <" + attest_email + ">"
|
||||
let email_body: String = "{\"from\":\"Neuron <no-reply@neurontechnologies.ai>\",\"to\":[\"will.anderson@neurontechnologies.ai\"],\"subject\":\"" + str_replace(subject, "\"", "\\\"") + "\",\"text\":\"Plan: " + attest_plan + "\\nName: " + n_safe + "\\nEmail: " + e_safe + "\\nTime: " + attest_ts + "\\n\\nAttestation: " + t_safe + "\"}"
|
||||
let resp: String = http_post_auth("https://api.resend.com/emails", resend_key, email_body)
|
||||
println("[attest] email: " + resp)
|
||||
}
|
||||
let attest_subject: String = "Founding member attestation: " + attest_name + " <" + attest_email + ">"
|
||||
let attest_text_body: String = "Plan: " + attest_plan + "\n"
|
||||
+ "Name: " + attest_name + "\n"
|
||||
+ "Email: " + attest_email + "\n"
|
||||
+ "Time: " + attest_ts + "\n\n"
|
||||
+ "Attestation: " + attest_text
|
||||
let attest_send: String = send_email(
|
||||
"Neuron <no-reply@neurontechnologies.ai>",
|
||||
"will.anderson@neurontechnologies.ai",
|
||||
attest_subject,
|
||||
"",
|
||||
attest_text_body
|
||||
)
|
||||
println("[attest] " + attest_send)
|
||||
return "{\"ok\":true}"
|
||||
}
|
||||
|
||||
// ── Developer interest form ───────────────────────────────────────────────
|
||||
if str_eq(path, "/api/developer-interest") {
|
||||
let resend_key: String = state_get("__resend_api_key__")
|
||||
if !str_contains(body, "\"email\"") {
|
||||
return "{\"error\":\"invalid request\"}"
|
||||
}
|
||||
@@ -950,15 +1031,17 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
let dev_idea: String = json_get(body, "idea")
|
||||
waitlist_upsert(dev_email, dev_name, "developer", "developer-interest", dev_idea, "", 0)
|
||||
println("[developer-interest] " + body)
|
||||
if !str_eq(resend_key, "") {
|
||||
let email_body: String = "{\"from\":\"Neuron Developer Program <developers@neurontechnologies.ai>\",\"to\":[\"will.anderson@neurontechnologies.ai\"],\"subject\":\"Developer Interest\",\"text\":" + body + "}"
|
||||
let resp: String = http_post_auth(
|
||||
"https://api.resend.com/emails",
|
||||
resend_key,
|
||||
email_body
|
||||
)
|
||||
println("[developer-interest] resend: " + resp)
|
||||
}
|
||||
let dev_text: String = "Name: " + dev_name + "\n"
|
||||
+ "Email: " + dev_email + "\n\n"
|
||||
+ "Idea:\n" + dev_idea
|
||||
let dev_send: String = send_email(
|
||||
"Neuron Developer Program <developers@neurontechnologies.ai>",
|
||||
"will.anderson@neurontechnologies.ai",
|
||||
"Developer Interest: " + dev_name,
|
||||
"",
|
||||
dev_text
|
||||
)
|
||||
println("[developer-interest] " + dev_send)
|
||||
return "{\"received\":true}"
|
||||
}
|
||||
|
||||
@@ -1244,27 +1327,27 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
let ds_is_completed: Bool = str_eq(event_type, "form.completed")
|
||||
let ds_is_declined: Bool = str_eq(event_type, "form.declined")
|
||||
if ds_is_completed || ds_is_declined {
|
||||
let resend_key: String = state_get("__resend_api_key__")
|
||||
println("[docuseal] gate event=" + event_type + " key=" + (if str_eq(resend_key, "") { "empty" } else { "set" }))
|
||||
if !str_eq(resend_key, "") {
|
||||
let subject: String = if str_eq(event_type, "form.completed") {
|
||||
"DocuSeal: " + signer_email + " signed (#" + sub_id + ")"
|
||||
} else {
|
||||
"DocuSeal: " + signer_email + " declined (#" + sub_id + ")"
|
||||
}
|
||||
let html: String = "<p>" + name_safe + " <" + signer_email + "></p>"
|
||||
+ "<p>Submission #" + sub_id + " - " + event_type + " at " + event_ts + "</p>"
|
||||
+ "<pre style=\"font-family:monospace;font-size:11px;background:#f4f4f4;padding:12px;border-radius:6px\">"
|
||||
+ body_safe + "</pre>"
|
||||
let html_safe: String = str_replace(str_replace(html, "\\", "\\\\"), "\"", "\\\"")
|
||||
let subject_safe: String = str_replace(subject, "\"", "\\\"")
|
||||
let email_body: String = "{\"from\":\"DocuSeal <no-reply@neurontechnologies.ai>\","
|
||||
+ "\"to\":[\"will.anderson@neurontechnologies.ai\"],"
|
||||
+ "\"subject\":\"" + subject_safe + "\","
|
||||
+ "\"html\":\"" + html_safe + "\"}"
|
||||
let mail_resp: String = http_post_auth("https://api.resend.com/emails", resend_key, email_body)
|
||||
println("[docuseal] resend " + event_type + " -> " + mail_resp)
|
||||
let ds_subject: String = if str_eq(event_type, "form.completed") {
|
||||
"DocuSeal: " + signer_email + " signed (#" + sub_id + ")"
|
||||
} else {
|
||||
"DocuSeal: " + signer_email + " declined (#" + sub_id + ")"
|
||||
}
|
||||
// The send_email helper handles JSON-escape of html/text. We
|
||||
// pass the raw payload body (body_safe is already DB-escaped,
|
||||
// but send_email re-escapes for JSON, which is idempotent for
|
||||
// already-escaped backslash/quote sequences in HTML context).
|
||||
let ds_html: String = "<p>" + name_safe + " <" + signer_email + "></p>"
|
||||
+ "<p>Submission #" + sub_id + " - " + event_type + " at " + event_ts + "</p>"
|
||||
+ "<pre style=\"font-family:monospace;font-size:11px;background:#f4f4f4;padding:12px;border-radius:6px\">"
|
||||
+ body_safe + "</pre>"
|
||||
let ds_send: String = send_email(
|
||||
"DocuSeal <no-reply@neurontechnologies.ai>",
|
||||
"will.anderson@neurontechnologies.ai",
|
||||
ds_subject,
|
||||
ds_html,
|
||||
""
|
||||
)
|
||||
println("[docuseal] " + event_type + " " + ds_send)
|
||||
}
|
||||
|
||||
return "{\"ok\":true}"
|
||||
@@ -1316,7 +1399,14 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if !str_eq(sb_key, "") {
|
||||
let q_safe: String = str_replace(str_replace(question, "\\", "\\\\"), "\"", "\\\"")
|
||||
let a_safe: String = str_replace(str_replace(str_replace(str_replace(answer_plain, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n"), "\r", "\\r")
|
||||
let card_row: String = "{\"id\":\"" + id + "\",\"question\":\"" + q_safe + "\",\"answer\":\"" + a_safe + "\"}"
|
||||
// answer_html: marked.js-rendered HTML the client captured,
|
||||
// sanitized server-side. Storing it lets /said render the
|
||||
// same formatted preview as /share/<id>. Empty -> omit the
|
||||
// column (legacy fallback path).
|
||||
let html_sanitized: String = if str_eq(answer_html_raw, "") { "" } else { sanitize_share_html(answer_html_raw) }
|
||||
let h_safe: String = str_replace(str_replace(str_replace(str_replace(html_sanitized, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n"), "\r", "\\r")
|
||||
let html_field: String = if str_eq(html_sanitized, "") { "" } else { ",\"answer_html\":\"" + h_safe + "\"" }
|
||||
let card_row: String = "{\"id\":\"" + id + "\",\"question\":\"" + q_safe + "\",\"answer\":\"" + a_safe + "\"" + html_field + "}"
|
||||
let sb_resp: String = supabase_insert(sb_url, sb_key, "share_cards", card_row)
|
||||
println("[share] supabase insert " + id + " -> " + sb_resp)
|
||||
}
|
||||
@@ -1486,7 +1576,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
let cards_json = supabase_get(
|
||||
sb_url,
|
||||
sb_key,
|
||||
"share_cards?select=id,question,answer,score,upvotes,downvotes,created_at&order=score.desc,created_at.desc&limit=100"
|
||||
"share_cards?select=id,question,answer,answer_html,score,upvotes,downvotes,created_at&order=score.desc,created_at.desc&limit=100"
|
||||
)
|
||||
}
|
||||
return gallery_page(cards_json, sb_url, sb_anon)
|
||||
@@ -1630,13 +1720,17 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
let fam_row: String = "{\"parent_email\":\"" + pe_safe + "\",\"parent_user_id\":\"\",\"child_email\":\"" + ce_safe + "\",\"child_dob_year\":" + child_dob_year_str + ",\"status\":\"invited\",\"stripe_subscription_id\":\"" + sub_id + "\",\"invite_token\":\"" + invite_token + "\"}"
|
||||
let fam_insert_resp: String = supabase_insert(fam_sb_url, fam_sb_key, "family_members", fam_row)
|
||||
println("[family/invite] insert -> " + fam_insert_resp)
|
||||
// Send invite email if Resend available
|
||||
let fam_resend_key: String = state_get("__resend_api_key__")
|
||||
if !str_eq(fam_resend_key, "") {
|
||||
let invite_email_body: String = "{\"from\":\"Neuron <no-reply@neurontechnologies.ai>\",\"to\":[\"" + ce_safe + "\"],\"subject\":\"You have been invited to Neuron\",\"text\":\"You have been invited to join Neuron by " + pe_safe + ". Visit https://neurontechnologies.ai/account to set up your account.\"}"
|
||||
let invite_email_resp: String = http_post_auth("https://api.resend.com/emails", fam_resend_key, invite_email_body)
|
||||
println("[family/invite] email -> " + invite_email_resp)
|
||||
}
|
||||
// Send invite email
|
||||
let invite_text: String = "You have been invited to join Neuron by " + parent_email
|
||||
+ ". Visit https://neurontechnologies.ai/account to set up your account."
|
||||
let invite_send: String = send_email(
|
||||
"Neuron <no-reply@neurontechnologies.ai>",
|
||||
child_email,
|
||||
"You have been invited to Neuron",
|
||||
"",
|
||||
invite_text
|
||||
)
|
||||
println("[family/invite] " + invite_send)
|
||||
return "{\"ok\":true,\"invite_token\":\"" + invite_token + "\"}"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user