// The Founder's Break - RSVP handler.
//
// Fires on form.submitted (form_name "rsvp"). Deterministic on purpose: an LLM
// in a transactional email path is a reliability problem, not a feature. Every
// string below is a fixed template.
//
// env: CORBEN_KEY (email + crm scoped), LIST_ID, FROM, NOTIFY
// User code is compiled as `async function (ctx) { ... }`, so the sandbox
// capabilities arrive on ctx rather than as globals.
var event = ctx.event, env = ctx.env, fetch = ctx.fetch, log = ctx.log, kv = ctx.kv;
var API = "https://api.corben.world/mcp";
var VENUE = "Silver Q Billiards & Sports Bar";
var STREET = "225 W University Ave";
var CITY = "Gainesville, FL 32601";
var SITE = "https://foundersbreak.onetouch.studio";
var MAP = "https://www.google.com/maps/search/?api=1&query=Silver+Q+Billiards+225+W+University+Ave+Gainesville+FL+32601";
async function tool(name, args) {
var res = await fetch(API, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer " + env.CORBEN_KEY },
body: JSON.stringify({ tool: name, arguments: args }),
});
if (!res.ok) throw new Error(name + " failed: " + res.status + " " + String(res.text).slice(0, 200));
return res.json;
}
function esc(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&").replace(//g, ">")
.replace(/"/g, """).replace(/'/g, "'");
}
function prettyDate(iso) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso || "")) return "this Thursday";
var p = iso.split("-").map(Number);
var d = new Date(Date.UTC(p[0], p[1] - 1, p[2]));
return new Intl.DateTimeFormat("en-US", {
timeZone: "UTC", weekday: "long", month: "long", day: "numeric",
}).format(d);
}
var HERO = "https://nyc3.digitaloceanspaces.com/corben-sites/images/c23ea202-5442-41b4-9a1b-1586e81c50eb/5b98d309-7283-4202-bf43-263dbcec74c2/email-header-web.png";
var SITE_ID = "4ae63819-3f63-4fea-b0de-eb1e5befe607";
var PIXEL = "https://api.corben.world/analytics/open.gif";
// 1x1 open pixel. Opens skew high because Apple Mail Privacy Protection and
// Gmail's proxy prefetch images, so read it as a ceiling, not a headcount.
function openPixel(kind, who, when) {
return '';
}
var UNSUB = "https://api.corben.world/campaigns/unsubscribe?user_id=c23ea202-5442-41b4-9a1b-1586e81c50eb&email=";
var SANS = "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif";
// One shell for every outbound email so they all look like the site. `image`
// is optional: the internal notification does not need a photograph.
function shell(opts) {
var image = opts.image
? '
| ' + ' |
| ' +
' ' + esc(when) + ' ' + '7:00 to 9:00 PM ' + '' +
'' +
VENUE + ' Free entry. 18 and over, so bring an ID. ' + ' |
| ' + '' + label + '' + ' |
You are confirmed for this week, ' + esc(first) + '. Same place, same time.
' + marquee(when) + 'Bring whatever you are working on. Forward this to anyone who should be there.
' + button("See what to expect", SITE); await tool("send_email", { from: env.FROM, to: email, subject: "See you " + when, html: shell({ headline: "Confirmed for " + when.split(",")[0] + ".", preview: when + ", 7:00 PM at Silver Q. Free, 18+.", image: HERO, imageAlt: "A blueprint drawing of a pool table and the break shot.", body: backBody, unsubscribeFor: encodeURIComponent(email), pixel: openPixel('confirm-returning', encodeURIComponent(email), eventDate), }), text: "You are confirmed for this week, " + first + ".\n\n" + when + ", 7:00 to 9:00 PM\n" + VENUE + "\n" + STREET + ", " + CITY + "\nFree entry. 18 and over, so bring an ID.\n\n" + SITE, }); mailed = "returning"; } else { var confirmBody = 'You are on the list, ' + esc(first) + '. There is nothing else to do before then.
' + marquee(when) + 'Pool experience is not expected. Bring whatever you are working on and we will introduce you to the people worth meeting.
' + 'A reminder will follow the day before.
' + button("See what to expect", SITE); await tool("send_email", { from: env.FROM, to: email, subject: "You are on the list: The Founder's Break, " + when, html: shell({ headline: "You are on the list.", preview: when + ", 7:00 PM at Silver Q. Free, 18+.", image: HERO, imageAlt: "A blueprint drawing of a pool table and the break shot.", body: confirmBody, unsubscribeFor: encodeURIComponent(email), pixel: openPixel('confirm-first', encodeURIComponent(email), eventDate), }), text: "You are on the list, " + first + ". There is nothing else to do before then.\n\n" + when + ", 7:00 to 9:00 PM\n" + VENUE + "\n" + STREET + ", " + CITY + "\nFree entry. 18 and over, so bring an ID.\n\n" + "Pool experience is not expected. Bring whatever you are working on.\n" + "A reminder will follow the day before.\n\n" + SITE, }); mailed = "first time"; } await kv.set(seenKey, new Date().toISOString()); // 4. Internal heads-up. Plain on purpose, this one is for us. var line = function (label, value) { return '' + '' + label + ' ' + '' + esc(value) + '
'; }; var notifyBody = line("Email", email) + line("Building", building || "did not say") + line("Coming to", when) + line("Answer", (attending === "no" ? "NOT coming this week" : "coming") + (previous && changed ? " (changed from " + esc(previous) + ")" : "")) + line("Came from", viaEmail ? "confirmed reminder-email response" : "the site form") + line("Mailing list", listed ? "added" : "not added, check the logs") + 'Confirmation sent: ' + mailed + '.
'; await tool("send_email", { from: env.FROM, to: env.NOTIFY, subject: (name || email) + (attending === "no" ? " is OUT for " : " is coming on ") + when, html: shell({ headline: esc(name || "Someone") + (attending === "no" ? " cannot make it." : " RSVPed."), preview: (name || email) + " for " + when, body: notifyBody, unsubscribeFor: encodeURIComponent(env.NOTIFY), }), text: (name || "(no name)") + (attending === "no" ? " is NOT coming " : " RSVPed for ") + when + "\nEmail: " + email + "\nBuilding: " + (building || "did not say") + "\nList: " + (listed ? "added" : "not added"), }); return { ok: true, contact_id: contactId, listed: listed, event_date: eventDate, attending: attending, previous: previous || null, changed: changed, returning: returning, confirmation: mailed, via_email: viaEmail, };