feat(broker): actually send invite email via Postmark, return emailed flag
Broker now sends the invite email when body.email is provided and POSTMARK_API_KEY (or RESEND_API_KEY) is configured. Returns `emailed: boolean` so the CLI can honestly report whether the email was sent instead of falsely claiming success on link generation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5132,8 +5132,40 @@ async function handleCliMeshInvite(req: IncomingMessage, slug: string, res: Serv
|
|||||||
const baseUrl = process.env.APP_URL || "https://claudemesh.com";
|
const baseUrl = process.env.APP_URL || "https://claudemesh.com";
|
||||||
const url = `${baseUrl}/i/${code}`;
|
const url = `${baseUrl}/i/${code}`;
|
||||||
|
|
||||||
writeJson(res, 200, { url, code, expires_at: expiresAt.toISOString() });
|
// If an email was provided, send the invite link via Postmark/Resend.
|
||||||
log.info("mesh-invite", { route: "POST /cli/mesh/:slug/invite", slug, code, email: body.email, latency_ms: Date.now() - started });
|
let emailed = false;
|
||||||
|
if (body.email) {
|
||||||
|
const apiKey = process.env.POSTMARK_API_KEY ?? process.env.RESEND_API_KEY;
|
||||||
|
const fromAddr = process.env.EMAIL_FROM ?? "noreply@claudemesh.com";
|
||||||
|
if (apiKey) {
|
||||||
|
try {
|
||||||
|
const subject = `You've been invited to the "${m.name}" mesh on claudemesh`;
|
||||||
|
const text = `You've been invited to join the "${m.name}" mesh on claudemesh.\n\nAccept the invite:\n${url}\n\nThis link expires on ${expiresAt.toISOString()}.\n\nIf you didn't expect this, ignore this email.`;
|
||||||
|
const res = process.env.POSTMARK_API_KEY
|
||||||
|
? await fetch("https://api.postmarkapp.com/email", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", "X-Postmark-Server-Token": apiKey },
|
||||||
|
body: JSON.stringify({ From: fromAddr, To: body.email, Subject: subject, TextBody: text }),
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
})
|
||||||
|
: await fetch("https://api.resend.com/emails", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
||||||
|
body: JSON.stringify({ from: fromAddr, to: body.email, subject, text }),
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
});
|
||||||
|
emailed = res.ok;
|
||||||
|
if (!res.ok) log.warn("invite email send failed", { status: res.status, body: await res.text().catch(() => "") });
|
||||||
|
} catch (e) {
|
||||||
|
log.error("invite email send error", { error: e instanceof Error ? e.message : String(e) });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("invite email requested but no POSTMARK_API_KEY/RESEND_API_KEY configured");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJson(res, 200, { url, code, expires_at: expiresAt.toISOString(), emailed });
|
||||||
|
log.info("mesh-invite", { route: "POST /cli/mesh/:slug/invite", slug, code, email: body.email, emailed, latency_ms: Date.now() - started });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("mesh-invite", { error: e instanceof Error ? e.message : String(e), stack: e instanceof Error ? e.stack : undefined });
|
log.error("mesh-invite", { error: e instanceof Error ? e.message : String(e), stack: e instanceof Error ? e.stack : undefined });
|
||||||
writeJson(res, 500, { error: "Failed to create invite" });
|
writeJson(res, 500, { error: "Failed to create invite" });
|
||||||
|
|||||||
Reference in New Issue
Block a user