2439 lines
139 KiB
EmacsLisp
2439 lines
139 KiB
EmacsLisp
// main.el - Neuron landing page server.
|
|
//
|
|
// Serves the Neuron marketing landing page at port 3001.
|
|
// Written in El (the Engram language). Runs on the El VM (elvm).
|
|
//
|
|
// The El HTTP server intercepts GET / and serves __html_file__ directly.
|
|
// We generate the page at startup with El components, write it to
|
|
// src/index.html, and set __html_file__ to that path.
|
|
//
|
|
// This means the document is always El-generated - never hand-authored.
|
|
// The El runtime serves it with the correct Content-Type: text/html header.
|
|
//
|
|
// Execution:
|
|
// el run (builds + executes via the El VM)
|
|
//
|
|
// Routes:
|
|
// GET / → landing page (El-rendered, served by runtime)
|
|
// GET /api/health → {"status":"ok"}
|
|
// GET /api/founding-count → {"remaining":N,"sold":N,"total":N}
|
|
// GET /assets/* → static assets (served by runtime from src/assets/)
|
|
// GET /brand/* → brand assets via handle_request
|
|
// GET * → 404 JSON (non-/ paths not used by this SPA)
|
|
|
|
// el-html vessel — extern declarations (implementations in dist/elhtml_impl.c)
|
|
extern fn el_escape(s: String) -> String
|
|
extern fn el_text(s: String) -> String
|
|
extern fn el_attr(name: String, value: String) -> String
|
|
extern fn el_div(attrs: String, children: String) -> String
|
|
extern fn el_section(attrs: String, children: String) -> String
|
|
extern fn el_article(attrs: String, children: String) -> String
|
|
extern fn el_header(attrs: String, children: String) -> String
|
|
extern fn el_footer(attrs: String, children: String) -> String
|
|
extern fn el_main(attrs: String, children: String) -> String
|
|
extern fn el_nav(attrs: String, children: String) -> String
|
|
extern fn el_aside(attrs: String, children: String) -> String
|
|
extern fn el_ul(attrs: String, children: String) -> String
|
|
extern fn el_ol(attrs: String, children: String) -> String
|
|
extern fn el_li(attrs: String, children: String) -> String
|
|
extern fn el_p(attrs: String, children: String) -> String
|
|
extern fn el_span(attrs: String, children: String) -> String
|
|
extern fn el_form(attrs: String, children: String) -> String
|
|
extern fn el_h1(attrs: String, text: String) -> String
|
|
extern fn el_h2(attrs: String, text: String) -> String
|
|
extern fn el_h3(attrs: String, text: String) -> String
|
|
extern fn el_h4(attrs: String, text: String) -> String
|
|
extern fn el_button(attrs: String, label: String) -> String
|
|
extern fn el_a(href: String, attrs: String, children: String) -> String
|
|
extern fn el_input(type_attr: String, attrs: String) -> String
|
|
extern fn el_textarea(attrs: String, value: String) -> String
|
|
extern fn el_label(for_id: String, attrs: String, children: String) -> String
|
|
extern fn el_img(src: String, alt: String, attrs: String) -> String
|
|
extern fn el_strong(children: String) -> String
|
|
extern fn el_em(children: String) -> String
|
|
extern fn el_code(children: String) -> String
|
|
extern fn el_pre(attrs: String, children: String) -> String
|
|
extern fn el_hr() -> String
|
|
extern fn el_br() -> String
|
|
extern fn el_html_doc(lang: String, head_html: String, body_html: String) -> String
|
|
extern fn el_meta(name: String, content: String) -> String
|
|
extern fn el_meta_charset(charset: String) -> String
|
|
extern fn el_link_stylesheet(href: String) -> String
|
|
extern fn el_script_src(src: String, defer_load: Bool) -> String
|
|
extern fn el_script_inline(js: String) -> String
|
|
extern fn el_title(text: String) -> String
|
|
|
|
from nav import { nav }
|
|
from hero import { hero }
|
|
from pillars import { pillars }
|
|
from how_it_works import { how_it_works }
|
|
from inference import { inference }
|
|
from efficiency import { efficiency }
|
|
from comparison import { comparison }
|
|
from environmental import { environmental }
|
|
from enterprise import { enterprise }
|
|
from mission import { mission }
|
|
from local_first import { local_first }
|
|
from pricing import { pricing }
|
|
from marketplace import { marketplace }
|
|
from viral import { viral }
|
|
from footer import { footer }
|
|
from styles import { page_open, page_open_seo, page_close }
|
|
from about import { about_page }
|
|
from founding_badge import { founding_badge, founding_badge_css }
|
|
from terms import { terms_page }
|
|
from enterprise_terms import { enterprise_terms_page }
|
|
from checkout import { checkout_page }
|
|
from safety import { safety }
|
|
from gallery import { gallery_page }
|
|
from account import { account_page }
|
|
|
|
// ── Share-card HTML allowlist ─────────────────────────────────────────────────
|
|
//
|
|
// Tag-and-attribute allowlist passed to el_html_sanitize for /api/share and
|
|
// /share/<id>. Anything not on this list is dropped at the runtime level by
|
|
// the strict state-machine parser. The previous denylist sanitizer was
|
|
// retired (root-cause replacement, not a bandaid): it could be bypassed by
|
|
// a literal --> inside an attacker-supplied attribute value, and every new
|
|
// vector required a code change.
|
|
//
|
|
// Empty array means tag is allowed but no attributes survive. The sanitizer
|
|
// also validates `<a href>` schemes (only http/https/mailto/fragment/relative
|
|
// pass) and drops the entire subtree of script/style/iframe/object/embed/
|
|
// form regardless of allowlist contents.
|
|
let default_share_allowlist: String = "{\"p\":[],\"br\":[],\"strong\":[],\"em\":[],\"u\":[],\"s\":[],\"code\":[],\"pre\":[],\"ul\":[],\"ol\":[],\"li\":[],\"h1\":[],\"h2\":[],\"h3\":[],\"h4\":[],\"blockquote\":[],\"a\":[\"href\",\"title\"]}"
|
|
|
|
// ── Founding counter ──────────────────────────────────────────────────────────
|
|
|
|
let FOUNDING_TOTAL: Int = 1000
|
|
let FOUNDING_SOLD: Int = 0 // no artificial floor - real count comes from Stripe
|
|
|
|
// ── Founding count helpers ─────────────────────────────────────────────────────
|
|
|
|
fn get_sold() -> Int {
|
|
let s: String = state_get("__founding_sold__")
|
|
if str_eq(s, "") {
|
|
return FOUNDING_SOLD
|
|
}
|
|
return str_to_int(s)
|
|
}
|
|
|
|
fn get_total() -> Int {
|
|
let s: String = state_get("__founding_total__")
|
|
if str_eq(s, "") {
|
|
return FOUNDING_TOTAL
|
|
}
|
|
return str_to_int(s)
|
|
}
|
|
|
|
// fetch_founding_count_stripe — queries Stripe PaymentIntents search for the
|
|
// real founding count. Uses the secret key with Bearer auth. Falls back to the
|
|
// FOUNDING_SOLD floor if Stripe is unavailable or not yet configured.
|
|
fn fetch_founding_count_stripe(stripe_key: String) -> Int {
|
|
if str_eq(stripe_key, "") {
|
|
return FOUNDING_SOLD
|
|
}
|
|
let count: Int = 0
|
|
|
|
// 1) PaymentIntents (Stripe Elements path - this is what the live site uses)
|
|
let pi_url: String = "https://api.stripe.com/v1/payment_intents?limit=100"
|
|
let pi_resp: String = http_get_auth(pi_url, stripe_key)
|
|
let pi_data: String = if str_eq(pi_resp, "") { "" } else { json_get_raw(pi_resp, "data") }
|
|
if !str_eq(pi_data, "") {
|
|
let pi_total: Int = json_array_len(pi_data)
|
|
let pi_idx: Int = 0
|
|
while pi_idx < pi_total {
|
|
let pi: String = json_array_get(pi_data, pi_idx)
|
|
let status: String = json_get(pi, "status")
|
|
let metadata: String = json_get_raw(pi, "metadata")
|
|
let plan: String = if str_eq(metadata, "") { "" } else { json_get(metadata, "plan") }
|
|
// Count only fully-charged founding PIs that are not refunded
|
|
let amt_refunded: String = json_get(pi, "amount_refunded")
|
|
let refunded_n: Int = str_to_int(amt_refunded)
|
|
if str_eq(status, "succeeded") && str_eq(plan, "founding") && refunded_n <= 0 {
|
|
let count = count + 1
|
|
}
|
|
let pi_idx = pi_idx + 1
|
|
}
|
|
}
|
|
|
|
// 2) Hosted Checkout Sessions (legacy /api/checkout path - kept for completeness)
|
|
let cs_url: String = "https://api.stripe.com/v1/checkout/sessions?status=complete&limit=100"
|
|
let cs_resp: String = http_get_auth(cs_url, stripe_key)
|
|
let cs_data: String = if str_eq(cs_resp, "") { "" } else { json_get_raw(cs_resp, "data") }
|
|
if !str_eq(cs_data, "") {
|
|
let cs_total: Int = json_array_len(cs_data)
|
|
let cs_idx: Int = 0
|
|
while cs_idx < cs_total {
|
|
let session: String = json_array_get(cs_data, cs_idx)
|
|
let metadata: String = json_get_raw(session, "metadata")
|
|
let plan: String = if str_eq(metadata, "") { "" } else { json_get(metadata, "plan") }
|
|
if str_eq(plan, "founding") {
|
|
let count = count + 1
|
|
}
|
|
let cs_idx = cs_idx + 1
|
|
}
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
// load_founding_count — reads from persist file first (survives restarts),
|
|
// falls back to Stripe query if file doesn't exist yet.
|
|
fn load_founding_count(sold_file: String, stripe_key: String) -> Int {
|
|
if fs_exists(sold_file) {
|
|
let s: String = str_trim(fs_read(sold_file))
|
|
let n: Int = str_to_int(s)
|
|
if n > 0 {
|
|
return n
|
|
}
|
|
}
|
|
return fetch_founding_count_stripe(stripe_key)
|
|
}
|
|
|
|
// persist_founding_count — writes current sold count to disk so it survives
|
|
// server restarts. Called at startup and on every Stripe webhook increment.
|
|
fn persist_founding_count(sold: Int) {
|
|
let sold_file: String = state_get("__founding_sold_file__")
|
|
if !str_eq(sold_file, "") {
|
|
fs_write(sold_file, int_to_str(sold))
|
|
}
|
|
}
|
|
|
|
// ── Page assembly ─────────────────────────────────────────────────────────────
|
|
|
|
fn page(sold: Int, total: Int) -> String {
|
|
// Page order is the source of truth; nav must mirror it section-for-section.
|
|
// Pricing is intentionally last so a visitor can scroll the whole story
|
|
// (what / how / why / who-it's-for / safety / planet) before being asked
|
|
// to commit to a plan.
|
|
return {page_open()}{nav()}{hero()}{pillars()}{how_it_works()}{inference()}{efficiency()}{comparison()}{enterprise()}{mission()}{local_first()}{safety()}{environmental()}{marketplace()}{viral()}{pricing(sold, total)}{footer()}{page_close()}
|
|
}
|
|
|
|
// ── Share card page ───────────────────────────────────────────────────────────
|
|
|
|
fn share_card_page(question: String, answer_plain: String, answer_html_in: String, id: String) -> String {
|
|
let q_html: String = str_replace(str_replace(str_replace(question, "&", "&"), "<", "<"), ">", ">")
|
|
// answer_html_in is sanitized, marked.js-rendered HTML. Fall back to
|
|
// escaped plaintext when the caller didn't supply rendered HTML (legacy).
|
|
let a_html: String = if str_eq(answer_html_in, "") {
|
|
str_replace(str_replace(str_replace(answer_plain, "&", "&"), "<", "<"), ">", ">")
|
|
} else {
|
|
el_html_sanitize(answer_html_in, default_share_allowlist)
|
|
}
|
|
// Use plaintext for og:description so social previews are readable.
|
|
let answer: String = answer_plain
|
|
let og_desc: String = str_slice(answer, 0, 140)
|
|
let base_url: String = state_get("__origin__")
|
|
let card_url: String = base_url + "/share/" + id
|
|
// Pre-built share hrefs — ID is digits so no URL encoding needed
|
|
let share_text: String = "The+AI+that+remembers+you+%E2%80%94+things+it+said%3A"
|
|
let x_href: String = "https://twitter.com/intent/tweet?url=" + card_url + "&text=" + share_text
|
|
let li_href: String = "https://www.linkedin.com/sharing/share-offsite/?url=" + card_url
|
|
let fb_href: String = "https://www.facebook.com/sharer/sharer.php?u=" + card_url
|
|
let wa_href: String = "https://wa.me/?text=" + share_text + "%20" + card_url
|
|
// TikTok and Snapchat have no web URL share scheme — use clipboard copy
|
|
let tiktok_copy_text: String = "Copied+%E2%80%94+paste+into+TikTok"
|
|
let snap_copy_text: String = "Copied+%E2%80%94+paste+into+Snapchat"
|
|
|
|
let cfg_js: String = "window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.id=\"" + id + "\";window.NEURON_CFG.card_url=\"" + card_url + "\";"
|
|
|
|
// ── Head ──────────────────────────────────────────────────────────────────
|
|
|
|
let share_css: String = "*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}"
|
|
+ ":root{--navy:#0052A0;--navy-dark:#003d7a;--t1:#0D0D14;--t2:#3A3A4A;--t3:#6B6B7E;--bg:#FAFAF8}"
|
|
+ "body{font-family:'IBM Plex Sans',system-ui,sans-serif;background:var(--bg);color:var(--t1);min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem 1rem}"
|
|
+ "body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;background-image:linear-gradient(rgba(0,0,0,.018) 1px,transparent 1px),linear-gradient(90deg,rgba(0,0,0,.018) 1px,transparent 1px);background-size:48px 48px}"
|
|
+ ".page{width:100%;max-width:580px;display:flex;flex-direction:column;gap:1.75rem;position:relative;z-index:1}"
|
|
+ ".page-header{display:flex;align-items:center;justify-content:space-between;gap:1rem}"
|
|
+ ".wordmark img{height:22px;width:auto;display:block}"
|
|
+ ".eyebrow{font-size:.65rem;font-weight:400;letter-spacing:.16em;text-transform:uppercase;color:var(--t3)}"
|
|
+ ".chat-frame{background:#fff;border:1px solid rgba(0,0,0,.09);box-shadow:0 4px 32px rgba(0,0,0,.07),0 1px 4px rgba(0,0,0,.04);padding:1.5rem;display:flex;flex-direction:column;gap:1rem}"
|
|
+ ".chat-row-user{display:flex;flex-direction:row-reverse}"
|
|
+ ".chat-row-ai{display:flex;flex-direction:row;align-items:flex-end;gap:.625rem}"
|
|
+ ".bubble-user{background:#0052A0;color:#fff;border-radius:18px 18px 4px 18px;padding:11px 15px;max-width:78%;font-size:.875rem;line-height:1.55;word-break:break-word}"
|
|
+ ".bubble-ai{background:var(--bg);color:var(--t1);border:1px solid rgba(0,0,0,.07);border-radius:18px 18px 18px 4px;padding:11px 15px;max-width:88%;font-size:.875rem;font-weight:300;line-height:1.65;word-break:break-word;box-shadow:0 2px 6px rgba(0,0,0,.05)}"
|
|
+ ".bubble-ai p{margin:0}"
|
|
+ ".bubble-ai p+p{margin-top:.6rem}"
|
|
+ ".bubble-ai ul,.bubble-ai ol{margin:.5rem 0 .5rem 1.25rem;padding:0}"
|
|
+ ".bubble-ai li+li{margin-top:.25rem}"
|
|
+ ".bubble-ai strong{font-weight:600}"
|
|
+ ".bubble-ai em{font-style:italic}"
|
|
+ ".bubble-ai code{font-family:'IBM Plex Mono','Menlo',monospace;font-size:.8rem;background:rgba(0,0,0,.05);padding:1px 4px;border-radius:3px}"
|
|
+ ".bubble-ai pre{background:rgba(0,0,0,.05);padding:.75rem;border-radius:6px;overflow-x:auto;font-size:.8rem;margin:.5rem 0}"
|
|
+ ".bubble-ai pre code{background:none;padding:0}"
|
|
+ ".bubble-ai blockquote{border-left:3px solid rgba(0,82,160,.3);margin:.5rem 0;padding:.25rem 0 .25rem .75rem;color:var(--t2)}"
|
|
+ ".bubble-ai h1,.bubble-ai h2,.bubble-ai h3,.bubble-ai h4{font-weight:600;margin:.5rem 0 .25rem}"
|
|
+ ".bubble-ai h1{font-size:1.05rem}.bubble-ai h2{font-size:1rem}.bubble-ai h3{font-size:.95rem}.bubble-ai h4{font-size:.9rem}"
|
|
+ ".bubble-ai a{color:var(--navy);text-decoration:underline}"
|
|
+ ".ai-col{display:flex;flex-direction:column;gap:.25rem}"
|
|
+ ".ai-label{font-size:.6rem;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--navy)}"
|
|
+ ".avatar{width:26px;height:26px;border-radius:50%;flex-shrink:0;background:#fff;border:1px solid rgba(0,82,160,.15);display:flex;align-items:center;justify-content:center}"
|
|
+ ".avatar img{width:14px;height:14px;object-fit:contain}"
|
|
+ ".vote-row{display:flex;align-items:center;gap:.75rem}"
|
|
+ ".vote-label{font-size:.65rem;font-weight:400;letter-spacing:.12em;text-transform:uppercase;color:var(--t3)}"
|
|
+ ".vote-btn{background:none;border:1px solid rgba(0,0,0,.12);cursor:pointer;padding:.3rem .6rem;font-size:.8rem;color:var(--t2);transition:all .15s;line-height:1}"
|
|
+ ".vote-btn:hover{border-color:var(--navy);color:var(--navy)}"
|
|
+ ".vote-btn.voted-up{background:var(--navy);color:#fff;border-color:var(--navy)}"
|
|
+ ".vote-btn.voted-down{background:#f0f0ec;color:var(--t3);border-color:rgba(0,0,0,.12)}"
|
|
+ ".vote-count{font-size:.8rem;font-weight:500;color:var(--t1);min-width:1.5rem;text-align:center}"
|
|
+ ".share-section{display:flex;flex-direction:column;gap:.625rem}"
|
|
+ ".share-label{font-size:.65rem;font-weight:500;letter-spacing:.12em;text-transform:uppercase;color:var(--t3)}"
|
|
+ ".share-row{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}"
|
|
+ ".share-btn{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;border:none;text-decoration:none;cursor:pointer;transition:opacity .15s,transform .15s;flex-shrink:0}"
|
|
+ ".share-btn:hover{opacity:.85;transform:scale(1.08)}"
|
|
+ ".share-btn.copied{outline:2px solid var(--navy)}"
|
|
+ ".divider{height:1px;background:rgba(0,0,0,.07)}"
|
|
+ ".footer-row{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap}"
|
|
+ ".footer-note{font-size:.75rem;color:var(--t3)}"
|
|
+ ".cta-btn{display:inline-flex;align-items:center;gap:.5rem;background:var(--navy);color:#fff;text-decoration:none;font-size:.7rem;font-weight:500;letter-spacing:.14em;text-transform:uppercase;padding:.7rem 1.4rem;white-space:nowrap;transition:background .15s}"
|
|
+ ".cta-btn:hover{background:var(--navy-dark)}"
|
|
+ "@media(max-width:480px){.chat-frame{padding:1.25rem}.footer-row{flex-direction:column;align-items:flex-start}}"
|
|
|
|
let head_html: String = el_meta_charset("UTF-8")
|
|
+ "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />"
|
|
+ el_title("Things Neuron Said")
|
|
+ "<meta property=\"og:title\" content=\"Things Neuron Said\" />"
|
|
+ "<meta property=\"og:description\" content=\"" + el_escape(og_desc) + "\" />"
|
|
+ "<meta property=\"og:image\" content=\"https://neurontechnologies.ai/assets/brand/neuron-wordmark-on-light@2x.png\" />"
|
|
+ "<meta property=\"og:type\" content=\"website\" />"
|
|
+ "<meta property=\"og:url\" content=\"" + el_escape(card_url) + "\" />"
|
|
+ "<meta name=\"twitter:card\" content=\"summary_large_image\" />"
|
|
+ "<meta name=\"twitter:title\" content=\"Things Neuron Said\" />"
|
|
+ "<meta name=\"twitter:description\" content=\"" + el_escape(og_desc) + "\" />"
|
|
+ "<meta name=\"twitter:image\" content=\"https://neurontechnologies.ai/assets/brand/neuron-wordmark-on-light@2x.png\" />"
|
|
+ "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />"
|
|
+ "<link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&display=swap\" rel=\"stylesheet\" />"
|
|
+ "<style>" + share_css + "</style>"
|
|
|
|
// ── Body ──────────────────────────────────────────────────────────────────
|
|
|
|
let wordmark_link: String = el_a(
|
|
"https://neurontechnologies.ai",
|
|
"class=\"wordmark\"",
|
|
el_img(
|
|
"/assets/brand/neuron-wordmark-on-light.png",
|
|
"Neuron",
|
|
"srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" height=\"22\""
|
|
)
|
|
)
|
|
|
|
let page_header: String = el_div(
|
|
"class=\"page-header\"",
|
|
wordmark_link + el_span("class=\"eyebrow\"", "Things Neuron Said")
|
|
)
|
|
|
|
let user_bubble: String = el_div("class=\"chat-row-user\"",
|
|
el_div("class=\"bubble-user\"", q_html)
|
|
)
|
|
|
|
let avatar: String = el_div("class=\"avatar\"",
|
|
el_img("/assets/neuron-icon.png", "", "")
|
|
)
|
|
|
|
let ai_col: String = el_div("class=\"ai-col\"",
|
|
el_span("class=\"ai-label\"", "Neuron")
|
|
+ el_div("class=\"bubble-ai\"", a_html)
|
|
)
|
|
|
|
let ai_bubble: String = el_div("class=\"chat-row-ai\"", avatar + ai_col)
|
|
|
|
let chat_frame: String = el_div("class=\"chat-frame\"", user_bubble + ai_bubble)
|
|
|
|
let vote_row: String = el_div(
|
|
"class=\"vote-row\"",
|
|
el_span("class=\"vote-label\"", "Helpful?")
|
|
+ "<button class=\"vote-btn\" id=\"vote-up\">▲</button>"
|
|
+ el_span("class=\"vote-count\" id=\"vote-score\"", "0")
|
|
+ "<button class=\"vote-btn\" id=\"vote-down\">▼</button>"
|
|
)
|
|
|
|
let share_row: String = el_div(
|
|
"class=\"share-row\"",
|
|
el_a(x_href, "target=\"_blank\" rel=\"noopener\" class=\"share-btn\" title=\"Post on X\" style=\"background:#000\"",
|
|
el_img("/assets/social/x.svg", "X", "width=\"18\" height=\"18\"")
|
|
)
|
|
+ el_a(li_href, "target=\"_blank\" rel=\"noopener\" class=\"share-btn\" title=\"Share on LinkedIn\" style=\"background:transparent\"",
|
|
el_img("/assets/social/linkedin.png", "LinkedIn", "width=\"18\" height=\"18\"")
|
|
)
|
|
+ el_a(fb_href, "target=\"_blank\" rel=\"noopener\" class=\"share-btn\" title=\"Share on Facebook\" style=\"background:#1877F2\"",
|
|
el_img("/assets/social/facebook.svg", "Facebook", "width=\"18\" height=\"18\"")
|
|
)
|
|
+ el_a(wa_href, "target=\"_blank\" rel=\"noopener\" class=\"share-btn\" title=\"Send via WhatsApp\" style=\"background:#25D366\"",
|
|
el_img("/assets/social/whatsapp.svg", "WhatsApp", "width=\"18\" height=\"18\"")
|
|
)
|
|
+ "<button class=\"share-btn\" id=\"copy-tiktok\" onclick=\"copyForPlatform('tiktok', this)\" title=\"Copy for TikTok\" style=\"background:#010101;border:none\">"
|
|
+ el_img("/assets/social/tiktok.svg", "TikTok", "width=\"18\" height=\"18\"")
|
|
+ "</button>"
|
|
+ "<button class=\"share-btn\" id=\"copy-snapchat\" onclick=\"copyForPlatform('snapchat', this)\" title=\"Copy for Snapchat\" style=\"background:#FFFC00;border:none\">"
|
|
+ el_img("/assets/social/snapchat.svg", "Snapchat", "width=\"18\" height=\"18\"")
|
|
+ "</button>"
|
|
)
|
|
|
|
let share_section: String = el_div(
|
|
"class=\"share-section\"",
|
|
el_span("class=\"share-label\"", "Share") + share_row
|
|
)
|
|
|
|
let footer_row: String = el_div(
|
|
"class=\"footer-row\"",
|
|
el_span("class=\"footer-note\"", "From a live conversation with Neuron.")
|
|
+ el_a("https://neurontechnologies.ai", "class=\"cta-btn\"", "Try Neuron ↗")
|
|
)
|
|
|
|
let page_div: String = el_div(
|
|
"class=\"page\"",
|
|
page_header
|
|
+ chat_frame
|
|
+ vote_row
|
|
+ share_section
|
|
+ el_div("class=\"divider\"", "")
|
|
+ footer_row
|
|
)
|
|
|
|
let body_html: String = page_div
|
|
+ el_script_inline(cfg_js)
|
|
+ el_script_src("/js/main.js", true)
|
|
|
|
el_html_doc("en", head_html, body_html)
|
|
}
|
|
|
|
// ── Static asset serving ──────────────────────────────────────────────────────
|
|
|
|
// waitlist_upsert — writes a row to the Supabase waitlist table.
|
|
// member_num: pass 0 to omit, positive for founding member number.
|
|
fn waitlist_upsert(email: String, name: String, plan: String, source: String, attestation: String, user_agent: String, member_num: Int) {
|
|
let sb_url: String = state_get("__supabase_project_url__")
|
|
let sb_key: String = state_get("__supabase_service_key__")
|
|
if str_eq(sb_key, "") {
|
|
println("[waitlist] no service key — skipping Supabase write")
|
|
return ""
|
|
}
|
|
let e_safe: String = str_replace(str_replace(email, "\\", "\\\\"), "\"", "\\\"")
|
|
let n_safe: String = str_replace(str_replace(name, "\\", "\\\\"), "\"", "\\\"")
|
|
let a_safe: String = str_replace(str_replace(attestation, "\\", "\\\\"), "\"", "\\\"")
|
|
let ua_safe: String = str_replace(str_replace(user_agent, "\\", "\\\\"), "\"", "\\\"")
|
|
let num_field: String = if member_num > 0 { ",\"member_number\":" + int_to_str(member_num) } else { "" }
|
|
let row: String = "{\"email\":\"" + e_safe + "\",\"name\":\"" + n_safe + "\",\"plan\":\"" + plan + "\",\"source\":\"" + source + "\",\"attestation\":\"" + a_safe + "\",\"user_agent\":\"" + ua_safe + "\"" + num_field + "}"
|
|
let resp: String = supabase_insert(sb_url, sb_key, "waitlist", row)
|
|
println("[waitlist] supabase insert -> " + resp)
|
|
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 {
|
|
return ""
|
|
}
|
|
return fs_read(abs_path)
|
|
}
|
|
|
|
// ── Runtime config store (Phase 1) ────────────────────────────────────────────
|
|
//
|
|
// Reads a single (key, scope='prod') row from public.neuron_config in
|
|
// Supabase. Caches the unwrapped string value in process state for 60s
|
|
// before re-fetching. Empty string on miss / on Supabase error so callers
|
|
// can safely fall back to a hardcoded default.
|
|
//
|
|
// jsonb wire format: PostgREST returns the value column as JSON. A jsonb
|
|
// string like '"claude-sonnet-4-5"' arrives as the literal six-byte
|
|
// payload "claude-sonnet-4-5" - already unquoted by json_get. Numbers and
|
|
// objects are left as-is.
|
|
//
|
|
// Why this exists: chat.model used to be NEURON_LLM_0_MODEL on the Cloud
|
|
// Run revision. Swapping models meant a redeploy. With this row, swapping
|
|
// is one PATCH and a 60s wait. Phase 2 brings Realtime so propagation is
|
|
// near-instant.
|
|
fn config_get(key: String) -> String {
|
|
let cache_key: String = "__cfg__" + key
|
|
let cache_at: String = "__cfg_at__" + key
|
|
let cached: String = state_get(cache_key)
|
|
let cached_at_str: String = state_get(cache_at)
|
|
let now: Int = unix_timestamp()
|
|
let cached_at: Int = if str_eq(cached_at_str, "") { 0 } else { str_to_int(cached_at_str) }
|
|
if !str_eq(cached, "") && (now - cached_at) < 60 {
|
|
return cached
|
|
}
|
|
let sb_url: String = state_get("__supabase_project_url__")
|
|
let sb_key: String = state_get("__supabase_service_key__")
|
|
if str_eq(sb_url, "") || str_eq(sb_key, "") {
|
|
return cached
|
|
}
|
|
let q: String = "neuron_config?select=value&key=eq." + key + "&scope=eq.prod&limit=1"
|
|
let resp: String = supabase_get(sb_url, sb_key, q)
|
|
if str_eq(resp, "") || str_eq(resp, "[]") {
|
|
return cached
|
|
}
|
|
// PostgREST returns [{"value":<jsonb>}]. Pull the first row, then the
|
|
// value field. json_get unwraps a jsonb string to its bare payload.
|
|
let row: String = json_array_get(resp, 0)
|
|
if str_eq(row, "") {
|
|
return cached
|
|
}
|
|
let val: String = json_get(row, "value")
|
|
if str_eq(val, "") {
|
|
// value could be a non-string jsonb (number/array/object). Fall
|
|
// back to the raw form so callers at least see something.
|
|
let val_raw: String = json_get_raw(row, "value")
|
|
if str_eq(val_raw, "") {
|
|
return cached
|
|
}
|
|
state_set(cache_key, val_raw)
|
|
state_set(cache_at, int_to_str(now))
|
|
return val_raw
|
|
}
|
|
state_set(cache_key, val)
|
|
state_set(cache_at, int_to_str(now))
|
|
return val
|
|
}
|
|
|
|
// ── Request handler ───────────────────────────────────────────────────────────
|
|
//
|
|
// NOTE: GET / is intercepted by the El HTTP runtime before reaching this
|
|
// function - it serves __html_file__ directly with text/html.
|
|
// This handler covers /api/* and /brand/* routes.
|
|
|
|
fn handle_request_inner(method: String, path: String, headers: Map, body: String) -> String {
|
|
let src_dir: String = state_get("__src_dir__")
|
|
|
|
// ── Root — serve El-generated landing page ────────────────────────────────
|
|
if str_eq(path, "/") {
|
|
let index_path: String = state_get("__html_file__")
|
|
if !str_eq(index_path, "") {
|
|
return fs_read(index_path)
|
|
}
|
|
}
|
|
|
|
// ── llms.txt — for AI crawlers (Perplexity, ChatGPT, Google SGE) ─────────
|
|
if str_eq(path, "/llms.txt") {
|
|
return fs_read(src_dir + "/llms.txt")
|
|
}
|
|
|
|
// ── robots.txt ────────────────────────────────────────────────────────────
|
|
if str_eq(path, "/robots.txt") {
|
|
return "User-agent: *\nAllow: /\nDisallow: /checkout\nDisallow: /account\nDisallow: /api/\nSitemap: https://neurontechnologies.ai/sitemap.xml\n"
|
|
}
|
|
|
|
// ── sitemap.xml ───────────────────────────────────────────────────────────
|
|
if str_eq(path, "/sitemap.xml") {
|
|
let sitemap_body: String = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
|
+ "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n"
|
|
+ " <url><loc>https://neurontechnologies.ai/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>\n"
|
|
+ " <url><loc>https://neurontechnologies.ai/about</loc><changefreq>monthly</changefreq><priority>0.8</priority></url>\n"
|
|
+ " <url><loc>https://neurontechnologies.ai/legal/terms</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
|
|
+ " <url><loc>https://neurontechnologies.ai/legal/enterprise-terms</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
|
|
+ "</urlset>\n"
|
|
return http_response(200, "{\"Content-Type\":\"application/xml; charset=utf-8\"}", sitemap_body)
|
|
}
|
|
|
|
// ── About page ────────────────────────────────────────────────────────────
|
|
if str_eq(path, "/about") {
|
|
let about_path: String = state_get("__about_html_file__")
|
|
if !str_eq(about_path, "") {
|
|
return fs_read(about_path)
|
|
}
|
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
|
}
|
|
|
|
// ── Terms of Service ──────────────────────────────────────────────────────
|
|
if str_eq(path, "/legal/terms") {
|
|
let terms_path: String = state_get("__terms_html_file__")
|
|
if !str_eq(terms_path, "") {
|
|
return fs_read(terms_path)
|
|
}
|
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
|
}
|
|
|
|
// ── Enterprise Agreement ──────────────────────────────────────────────────
|
|
if str_eq(path, "/legal/enterprise-terms") {
|
|
let ent_path: String = state_get("__enterprise_terms_html_file__")
|
|
if !str_eq(ent_path, "") {
|
|
return fs_read(ent_path)
|
|
}
|
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
|
}
|
|
|
|
// ── Checkout page ─────────────────────────────────────────────────────────
|
|
if str_starts_with(path, "/checkout") {
|
|
let plan: String = "founding"
|
|
if str_contains(path, "plan=professional") {
|
|
plan = "professional"
|
|
}
|
|
if str_contains(path, "plan=free") {
|
|
plan = "free"
|
|
}
|
|
let pub_key: String = state_get("__stripe_publishable_key__")
|
|
let checkout_title: String = if str_eq(plan, "founding") {
|
|
"Founding Member Checkout — Neuron"
|
|
} else {
|
|
if str_eq(plan, "free") {
|
|
"Get Started Free — Neuron"
|
|
} else {
|
|
"Professional Plan Checkout — Neuron"
|
|
}
|
|
}
|
|
let checkout_desc: String = if str_eq(plan, "founding") {
|
|
"Secure your Founding Member spot. Pay once, $199 lifetime — Neuron inference included at launch, priced below the major APIs. First 1,000 only."
|
|
} else {
|
|
if str_eq(plan, "free") {
|
|
"Create your free Neuron account. A card verifies you're 18+ — you won't be charged. Your AI that remembers you, runs on your machine, never resets."
|
|
} else {
|
|
"Subscribe to Neuron Professional for $19/month. The AI that remembers you — persistent memory, runs locally, bring your own API keys."
|
|
}
|
|
}
|
|
return page_open_seo(checkout_title, checkout_desc, "/checkout", checkout_desc, "true") + checkout_page(plan, pub_key) + page_close()
|
|
}
|
|
|
|
// ── Stripe payment intent / setup intent ─────────────────────────────────
|
|
// body fields:
|
|
// plan: "founding" | "professional" | "free"
|
|
// timing: "now" | "later" (Professional only; Founding always charges)
|
|
// When timing == "later" we create a SetupIntent so the buyer's card is
|
|
// saved without being charged. A Subscription with trial_end at launch
|
|
// (Q3 2026) will be created later from the saved payment method.
|
|
if str_eq(path, "/api/payment-intent") {
|
|
let stripe_key: String = state_get("__stripe_secret_key__")
|
|
if str_eq(stripe_key, "") {
|
|
return "{\"__status__\":503,\"error\":\"Stripe not configured\"}"
|
|
}
|
|
let plan: String = "founding"
|
|
if str_contains(body, "\"professional\"") {
|
|
plan = "professional"
|
|
}
|
|
if str_contains(body, "\"free\"") {
|
|
plan = "free"
|
|
}
|
|
let timing: String = json_get_string(body, "timing")
|
|
if str_eq(timing, "") { let timing = "now" }
|
|
// Hard cap: block founding checkouts when 1,000 spots are filled
|
|
if str_eq(plan, "founding") {
|
|
let current_sold: Int = get_sold()
|
|
let total_spots: Int = get_total()
|
|
if current_sold >= total_spots {
|
|
return "{\"__status__\":410,\"error\":\"sold_out\",\"message\":\"All 1,000 Founding Member spots have been claimed.\"}"
|
|
}
|
|
}
|
|
let auth_header: String = "Bearer " + stripe_key
|
|
|
|
// Find-or-create Stripe Customer by email upfront so every intent
|
|
// is attached to an existing customer — prevents duplicate customers.
|
|
let pi_email: String = json_get_string(body, "email")
|
|
let pi_name: String = json_get_string(body, "name")
|
|
let pi_cus_id: String = ""
|
|
if !str_eq(pi_email, "") {
|
|
let pi_email_enc: String = str_replace(str_replace(pi_email, "@", "%40"), "+", "%2B")
|
|
let pi_search_url: String = "https://api.stripe.com/v1/customers/search?query=email%3A%22" + pi_email_enc + "%22&limit=1"
|
|
let pi_search: String = http_get_auth(pi_search_url, auth_header)
|
|
let pi_cus_id = json_get_string(pi_search, "id")
|
|
if str_eq(pi_cus_id, "") {
|
|
let pi_name_enc: String = str_replace(pi_name, " ", "%20")
|
|
let pi_cus_body: String = "email=" + pi_email_enc
|
|
+ "&name=" + pi_name_enc
|
|
+ "&metadata[plan]=" + plan
|
|
+ "&metadata[source]=neuron-checkout"
|
|
let pi_cus_resp: String = http_post_form_auth("https://api.stripe.com/v1/customers", pi_cus_body, auth_header)
|
|
let pi_cus_id = json_get_string(pi_cus_resp, "id")
|
|
}
|
|
}
|
|
|
|
// Free tier: creates a SetupIntent for age verification (18+ requirement).
|
|
// No charge — but the user must provide a valid payment method.
|
|
if str_eq(plan, "free") {
|
|
let free_si_body: String = "automatic_payment_methods[enabled]=true"
|
|
+ "&usage=off_session"
|
|
+ "&metadata[plan]=free"
|
|
+ "&metadata[purpose]=age_verification"
|
|
let free_si_body = if !str_eq(pi_cus_id, "") { free_si_body + "&customer=" + pi_cus_id } else { free_si_body }
|
|
let free_si_resp: String = http_post_form_auth(
|
|
"https://api.stripe.com/v1/setup_intents",
|
|
free_si_body,
|
|
auth_header)
|
|
if str_starts_with(free_si_resp, "{") {
|
|
let inner: String = str_slice(free_si_resp, 1, str_len(free_si_resp))
|
|
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
|
|
}
|
|
return free_si_resp
|
|
}
|
|
|
|
// Setup-mode path: save payment method, do not charge. Only valid
|
|
// for Professional (Founding is one-shot lifetime, charges immediately).
|
|
if str_eq(plan, "professional") && str_eq(timing, "later") {
|
|
let si_body: String = "automatic_payment_methods[enabled]=true"
|
|
+ "&usage=off_session"
|
|
+ "&metadata[plan]=" + plan
|
|
+ "&metadata[hold_until]=launch"
|
|
+ "&metadata[launch_target]=2026-09-01"
|
|
let si_body = if !str_eq(pi_cus_id, "") { si_body + "&customer=" + pi_cus_id } else { si_body }
|
|
let si_resp: String = http_post_form_auth(
|
|
"https://api.stripe.com/v1/setup_intents",
|
|
si_body,
|
|
auth_header)
|
|
// Splice in setup_mode marker so the frontend knows to call
|
|
// stripe.confirmSetup instead of stripe.confirmPayment.
|
|
if str_starts_with(si_resp, "{") {
|
|
let inner: String = str_slice(si_resp, 1, str_len(si_resp))
|
|
return "{\"setup_mode\":true,\"plan\":\"" + plan + "\"," + inner
|
|
}
|
|
return si_resp
|
|
}
|
|
|
|
let amount: String = "19900"
|
|
if str_eq(plan, "professional") {
|
|
amount = "1900"
|
|
}
|
|
let pi_body: String = "amount=" + amount
|
|
+ "¤cy=usd"
|
|
+ "&automatic_payment_methods[enabled]=true"
|
|
+ "&metadata[plan]=" + plan
|
|
+ "&metadata[timing]=" + timing
|
|
let pi_body = if !str_eq(pi_cus_id, "") { pi_body + "&customer=" + pi_cus_id } else { pi_body }
|
|
let response: String = http_post_form_auth(
|
|
"https://api.stripe.com/v1/payment_intents",
|
|
pi_body,
|
|
auth_header
|
|
)
|
|
return response
|
|
}
|
|
|
|
// ── Link Stripe Customer to PaymentIntent + Supabase waitlist row ────────
|
|
// Called from checkout.js right before stripe.confirmPayment. Three jobs:
|
|
// 1. Find or create a Stripe Customer for this email (so they aren't a Guest)
|
|
// 2. Attach the Customer to the live PaymentIntent
|
|
// 3. Stamp stripe_customer_id + plan on the Supabase waitlist row (upsert by email)
|
|
// Non-fatal: if any step errors, we still return 200 so the buyer can complete
|
|
// payment. The Stripe webhook ( /api/webhooks/stripe ) re-links server-side on
|
|
// payment_intent.succeeded as a backstop.
|
|
if str_eq(path, "/api/link-customer") {
|
|
let stripe_key: String = state_get("__stripe_secret_key__")
|
|
if str_eq(stripe_key, "") {
|
|
return "{\"linked\":false,\"error\":\"stripe_not_configured\"}"
|
|
}
|
|
let lc_pi_id: String = json_get_string(body, "pi_id")
|
|
let lc_email: String = json_get_string(body, "email")
|
|
let lc_name: String = json_get_string(body, "name")
|
|
let lc_plan: String = json_get_string(body, "plan")
|
|
let lc_supa: String = json_get_string(body, "supabase_user_id")
|
|
if str_eq(lc_pi_id, "") || str_eq(lc_email, "") {
|
|
return "{\"linked\":false,\"error\":\"missing_pi_or_email\"}"
|
|
}
|
|
let lc_auth: String = "Bearer " + stripe_key
|
|
// URL-encode email so + and @ survive the form-body
|
|
let lc_email_enc: String = str_replace(str_replace(lc_email, "@", "%40"), "+", "%2B")
|
|
let lc_name_enc: String = str_replace(lc_name, " ", "%20")
|
|
|
|
// 1. Search existing customers by email
|
|
let lc_search_url: String = "https://api.stripe.com/v1/customers/search?query=email%3A%22" + lc_email_enc + "%22&limit=1"
|
|
let lc_search: String = http_get_auth(lc_search_url, lc_auth)
|
|
let lc_cus_id: String = json_get_string(lc_search, "id")
|
|
|
|
// 2. If none, create one. We always include supabase_user_id so the
|
|
// Stripe Customer cross-references back to our auth identity.
|
|
if str_eq(lc_cus_id, "") {
|
|
let lc_create_body: String = "email=" + lc_email_enc
|
|
+ "&name=" + lc_name_enc
|
|
+ "&description=" + lc_name_enc
|
|
+ "&metadata[plan]=" + lc_plan
|
|
+ "&metadata[source]=neuron-checkout"
|
|
+ "&metadata[supabase_user_id]=" + lc_supa
|
|
let lc_create: String = http_post_form_auth("https://api.stripe.com/v1/customers", lc_create_body, lc_auth)
|
|
let lc_cus_id = json_get_string(lc_create, "id")
|
|
} else {
|
|
// Existing customer: stamp the supabase_user_id if we now know it
|
|
// (e.g. they signed in after a guest-style purchase).
|
|
if !str_eq(lc_supa, "") {
|
|
let lc_patch_body: String = "metadata[supabase_user_id]=" + lc_supa + "&metadata[plan]=" + lc_plan
|
|
let lc_patch_url: String = "https://api.stripe.com/v1/customers/" + lc_cus_id
|
|
let _lc_patch: String = http_post_form_auth(lc_patch_url, lc_patch_body, lc_auth)
|
|
}
|
|
}
|
|
|
|
// 3. Attach customer to the live Intent. SetupIntents (seti_*) and
|
|
// PaymentIntents (pi_*) live at different REST endpoints, so
|
|
// branch on the id prefix. SetupIntents do not accept
|
|
// receipt_email - that's a PaymentIntent-only field.
|
|
if !str_eq(lc_cus_id, "") {
|
|
let lc_is_setup: Bool = str_starts_with(lc_pi_id, "seti_")
|
|
let lc_attach_url: String = if lc_is_setup {
|
|
"https://api.stripe.com/v1/setup_intents/" + lc_pi_id
|
|
} else {
|
|
"https://api.stripe.com/v1/payment_intents/" + lc_pi_id
|
|
}
|
|
let lc_attach_body: String = if lc_is_setup {
|
|
"customer=" + lc_cus_id
|
|
} else {
|
|
"customer=" + lc_cus_id + "&receipt_email=" + lc_email_enc
|
|
}
|
|
let _lc_attach: String = http_post_form_auth(lc_attach_url, lc_attach_body, lc_auth)
|
|
}
|
|
|
|
// 4. Upsert the Supabase waitlist row so /account can find this purchase by email
|
|
let sb_url: String = state_get("__supabase_project_url__")
|
|
let sb_key: String = state_get("__supabase_service_key__")
|
|
if !str_eq(sb_url, "") && !str_eq(sb_key, "") && !str_eq(lc_cus_id, "") {
|
|
let lc_row: String = "{\"email\":\"" + lc_email + "\",\"name\":\"" + lc_name + "\",\"stripe_customer_id\":\"" + lc_cus_id + "\",\"plan\":\"" + lc_plan + "\"}"
|
|
let _wl_resp: String = supabase_insert(sb_url, sb_key, "waitlist?on_conflict=email", lc_row)
|
|
}
|
|
|
|
return "{\"linked\":true,\"customer_id\":\"" + lc_cus_id + "\"}"
|
|
}
|
|
|
|
// ── Health check ──────────────────────────────────────────────────────────
|
|
if str_eq(path, "/api/health") {
|
|
return "{\"status\":\"ok\",\"service\":\"neuron-web\"}"
|
|
}
|
|
|
|
// ── Admin: read-only config snapshot (Phase 1) ────────────────────────────
|
|
// POST { "admin_token": "<NEURON_ADMIN_TOKEN>" } - returns the full
|
|
// neuron_config table for verification. Phase 2 adds POST/PUT and an
|
|
// auth-gated admin page; for now this is the only surface.
|
|
//
|
|
// Token in the body keeps this consistent with the rest of /api/* (the
|
|
// El runtime does not surface request headers to the handler today).
|
|
if str_eq(path, "/api/admin/config") {
|
|
if !str_eq(method, "POST") {
|
|
return "{\"__status__\":405,\"error\":\"POST required\"}"
|
|
}
|
|
let admin_token_in: String = json_get(body, "admin_token")
|
|
let admin_token_expected: String = env("NEURON_ADMIN_TOKEN")
|
|
if str_eq(admin_token_expected, "") {
|
|
return "{\"__status__\":503,\"error\":\"admin_token_not_configured\"}"
|
|
}
|
|
if !str_eq(admin_token_in, admin_token_expected) {
|
|
return "{\"__status__\":401,\"error\":\"unauthorized\"}"
|
|
}
|
|
let ac_sb_url: String = state_get("__supabase_project_url__")
|
|
let ac_service: String = state_get("__supabase_service_key__")
|
|
if str_eq(ac_sb_url, "") || str_eq(ac_service, "") {
|
|
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
|
|
}
|
|
let ac_q: String = "neuron_config?select=key,value,scope,updated_at,updated_by&order=key"
|
|
let ac_resp: String = supabase_get(ac_sb_url, ac_service, ac_q)
|
|
if str_eq(ac_resp, "") {
|
|
return "{\"rows\":[]}"
|
|
}
|
|
return "{\"rows\":" + ac_resp + "}"
|
|
}
|
|
|
|
// ── My plan: server-side waitlist read with JWT verification ─────────────
|
|
// POST { "access_token": "<user_jwt>" }. We verify the JWT via Supabase
|
|
// /auth/v1/user, extract the email, then read the waitlist row with the
|
|
// SERVICE key (bypasses RLS). Canonical plan source for /account.
|
|
if str_eq(path, "/api/my-plan") {
|
|
let mp_jwt: String = json_get_string(body, "access_token")
|
|
if str_eq(mp_jwt, "") {
|
|
return "{\"__status__\":401,\"error\":\"missing_jwt\"}"
|
|
}
|
|
let mp_sb_url: String = state_get("__supabase_project_url__")
|
|
let mp_anon: String = state_get("__supabase_anon_key__")
|
|
let mp_service: String = state_get("__supabase_service_key__")
|
|
if str_eq(mp_sb_url, "") || str_eq(mp_anon, "") || str_eq(mp_service, "") {
|
|
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
|
|
}
|
|
let mp_user: String = supabase_auth_user(mp_sb_url, mp_anon, mp_jwt)
|
|
let mp_email: String = json_get(mp_user, "email")
|
|
if str_eq(mp_email, "") {
|
|
return "{\"__status__\":401,\"error\":\"invalid_jwt\"}"
|
|
}
|
|
let mp_email_safe: String = str_replace(str_replace(mp_email, "@", "%40"), "+", "%2B")
|
|
let mp_query: String = "waitlist?select=plan,member_number,source,created_at,stripe_customer_id,name&email=eq." + mp_email_safe + "&order=created_at.desc&limit=1"
|
|
let mp_resp: String = supabase_get(mp_sb_url, mp_service, mp_query)
|
|
if str_eq(mp_resp, "") || str_eq(mp_resp, "[]") {
|
|
return "{\"plan\":null,\"email\":\"" + mp_email + "\"}"
|
|
}
|
|
// Strip outer array brackets to return a plain object
|
|
if str_starts_with(mp_resp, "[") {
|
|
let mp_inner: String = str_slice(mp_resp, 1, str_len(mp_resp) - 1)
|
|
return mp_inner
|
|
}
|
|
return mp_resp
|
|
}
|
|
|
|
|
|
// ── Founding count ────────────────────────────────────────────────────────
|
|
// Live Stripe query is ~1s. Cache the result for FOUNDING_CACHE_TTL_S
|
|
// and serve cached values otherwise. The Stripe webhook bumps the
|
|
// counter on payment_intent.succeeded, so the cache going stale by
|
|
// 30s does not affect freshness for actual purchases.
|
|
if str_eq(path, "/api/founding-count") {
|
|
let now: Int = unix_timestamp()
|
|
let cached_at_str: String = state_get("__founding_cached_at__")
|
|
let cached_at: Int = if str_eq(cached_at_str, "") { 0 } else { str_to_int(cached_at_str) }
|
|
let ttl: Int = 30
|
|
if (now - cached_at) > ttl {
|
|
let stripe_key: String = state_get("__stripe_secret_key__")
|
|
let live_sold: Int = fetch_founding_count_stripe(stripe_key)
|
|
if live_sold > get_sold() {
|
|
state_set("__founding_sold__", int_to_str(live_sold))
|
|
persist_founding_count(live_sold)
|
|
}
|
|
state_set("__founding_cached_at__", int_to_str(now))
|
|
}
|
|
let sold: Int = get_sold()
|
|
let total: Int = get_total()
|
|
let remaining: Int = total - sold
|
|
let sold_s: String = int_to_str(sold)
|
|
let total_s: String = int_to_str(total)
|
|
let rem_s: String = int_to_str(remaining)
|
|
return "{\"sold\":" + sold_s + ",\"total\":" + total_s + ",\"remaining\":" + rem_s + "}"
|
|
}
|
|
|
|
// ── Static assets: /assets/* ──────────────────────────────────────────────
|
|
// Returns Cache-Control: public, max-age=31536000, immutable so Cloudflare
|
|
// caches these at the edge and never forwards subsequent requests to Cloud Run.
|
|
if str_starts_with(path, "/assets/") {
|
|
let rel: String = str_slice(path, 8, str_len(path))
|
|
let abs: String = src_dir + "/assets/" + rel
|
|
let content: String = read_asset(abs)
|
|
if str_eq(content, "") {
|
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
|
}
|
|
return http_response(200, static_asset_headers_json(), content)
|
|
}
|
|
|
|
// ── Compiled client-side JS: /js/* ───────────────────────────────────────
|
|
// Served from dist/js/ (compiled by elc --target=js at build time).
|
|
// LANDING_ROOT/js maps to the dist/js output directory in the image.
|
|
// Returns an http_response envelope with explicit Content-Type so the
|
|
// browser executes the file as JavaScript — http_detect_content_type()
|
|
// mis-identifies minified/obfuscated JS as JSON because many obfuscated
|
|
// bundles start with '[' (which is also a JSON array opener).
|
|
if str_starts_with(path, "/js/") {
|
|
let rel: String = str_slice(path, 4, str_len(path))
|
|
let abs: String = src_dir + "/js/" + rel
|
|
let content: String = read_asset(abs)
|
|
if str_eq(content, "") {
|
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
|
}
|
|
return http_response(200, js_headers_json(), content)
|
|
}
|
|
|
|
// ── Brand assets: /brand/* ────────────────────────────────────────────────
|
|
// Same long-lived cache policy as /assets/* — served from edge, not Cloud Run.
|
|
if str_starts_with(path, "/brand/") {
|
|
let rel: String = str_slice(path, 7, str_len(path))
|
|
let abs: String = src_dir + "/brand/" + rel
|
|
let content: String = read_asset(abs)
|
|
if str_eq(content, "") {
|
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
|
}
|
|
return http_response(200, static_asset_headers_json(), content)
|
|
}
|
|
|
|
// ── Stripe checkout ───────────────────────────────────────────────────────
|
|
if str_eq(path, "/api/checkout") {
|
|
let stripe_key: String = state_get("__stripe_secret_key__")
|
|
if str_eq(stripe_key, "") {
|
|
return "{\"__status__\":503,\"error\":\"Stripe not configured\"}"
|
|
}
|
|
let plan: String = "founding"
|
|
if str_contains(body, "\"professional\"") {
|
|
plan = "professional"
|
|
}
|
|
let origin: String = state_get("__origin__")
|
|
let price_id: String = ""
|
|
let mode: String = "subscription"
|
|
if str_eq(plan, "founding") {
|
|
price_id = state_get("__stripe_price_founding__")
|
|
mode = "payment"
|
|
}
|
|
if str_eq(plan, "professional") {
|
|
price_id = state_get("__stripe_price_professional__")
|
|
mode = "subscription"
|
|
}
|
|
if str_eq(price_id, "") {
|
|
return "{\"__status__\":503,\"error\":\"Plan price not configured\"}"
|
|
}
|
|
let form_body: String = "mode=" + mode
|
|
+ "&line_items[0][price]=" + price_id
|
|
+ "&line_items[0][quantity]=1"
|
|
+ "&success_url=" + origin + "/marketplace/success?session_id={CHECKOUT_SESSION_ID}"
|
|
+ "&cancel_url=" + origin + "/#pricing"
|
|
+ "&allow_promotion_codes=true"
|
|
+ "&metadata[plan]=" + plan
|
|
let auth_hdr: String = "Bearer " + stripe_key
|
|
let response: String = http_post_form_auth(
|
|
"https://api.stripe.com/v1/checkout/sessions",
|
|
form_body,
|
|
auth_hdr
|
|
)
|
|
if str_contains(response, "\"url\"") {
|
|
return response
|
|
}
|
|
return "{\"__status__\":500,\"error\":\"Stripe session creation failed\"}"
|
|
}
|
|
|
|
// ── Enterprise inquiry ────────────────────────────────────────────────────
|
|
if str_eq(path, "/api/enterprise-inquiry") {
|
|
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)
|
|
// 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}"
|
|
}
|
|
|
|
// ── Free tier waitlist ────────────────────────────────────────────────────
|
|
if str_eq(path, "/api/waitlist") {
|
|
if !str_eq(method, "POST") {
|
|
return "{\"error\":\"POST required\"}"
|
|
}
|
|
let wl_email: String = json_get(body, "email")
|
|
if str_eq(wl_email, "") {
|
|
return "{\"error\":\"email required\"}"
|
|
}
|
|
// Write to Supabase waitlist table
|
|
waitlist_upsert(wl_email, "", "free", "preorder", "", "", 0)
|
|
// Email notification
|
|
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}"
|
|
}
|
|
|
|
// ── Founding member attestation record ───────────────────────────────────
|
|
// Fired from the checkout form just before Stripe confirmPayment().
|
|
// Saves to GCS (attestations/<ts>-founding-<email>.json) and emails Will.
|
|
if str_eq(path, "/api/attest") {
|
|
if !str_eq(method, "POST") {
|
|
return "{\"error\":\"POST required\"}"
|
|
}
|
|
let attest_name: String = json_get(body, "name")
|
|
let attest_email: String = json_get(body, "email")
|
|
let attest_plan: String = json_get(body, "plan")
|
|
let attest_ts: String = json_get(body, "timestamp")
|
|
let attest_text: String = json_get(body, "attestation")
|
|
let attest_ua: String = json_get(body, "user_agent")
|
|
if str_eq(attest_email, "") {
|
|
return "{\"error\":\"email required\"}"
|
|
}
|
|
let n_safe: String = str_replace(str_replace(attest_name, "\\", "\\\\"), "\"", "\\\"")
|
|
let e_safe: String = str_replace(str_replace(attest_email, "\\", "\\\\"), "\"", "\\\"")
|
|
let t_safe: String = str_replace(str_replace(attest_text, "\\", "\\\\"), "\"", "\\\"")
|
|
let ua_safe: String = str_replace(str_replace(attest_ua, "\\", "\\\\"), "\"", "\\\"")
|
|
// Write to Supabase waitlist (attestation in dedicated column)
|
|
waitlist_upsert(attest_email, attest_name, attest_plan, "founding-attestation", attest_text, attest_ua, 0)
|
|
// Also save to GCS as immutable legal record.
|
|
// Written to the dedicated attestations bucket (GCS_ATTEST_BUCKET) which
|
|
// is private and separate from the public-read shares bucket.
|
|
let record: String = "{\"plan\":\"" + attest_plan + "\",\"name\":\"" + n_safe + "\",\"email\":\"" + e_safe + "\",\"timestamp\":\"" + attest_ts + "\",\"attestation\":\"" + t_safe + "\",\"user_agent\":\"" + ua_safe + "\"}"
|
|
let attest_bucket: String = env("GCS_ATTEST_BUCKET")
|
|
if str_eq(attest_bucket, "") {
|
|
// Fall back to share bucket with attestations/ prefix for legacy deploys
|
|
let attest_bucket = env("GCS_SHARE_BUCKET")
|
|
}
|
|
if !str_eq(attest_bucket, "") {
|
|
let attest_key: String = attest_ts + "-" + attest_email + ".json"
|
|
let gcs_ok: String = gcs_write(attest_bucket, attest_key, record)
|
|
println("[attest] gcs write " + attest_key + " -> " + gcs_ok)
|
|
}
|
|
// Email notification
|
|
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") {
|
|
if !str_contains(body, "\"email\"") {
|
|
return "{\"error\":\"invalid request\"}"
|
|
}
|
|
// Write to Supabase waitlist
|
|
let dev_email: String = json_get(body, "email")
|
|
let dev_name: String = json_get(body, "name")
|
|
let dev_idea: String = json_get(body, "idea")
|
|
waitlist_upsert(dev_email, dev_name, "developer", "developer-interest", dev_idea, "", 0)
|
|
println("[developer-interest] " + body)
|
|
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}"
|
|
}
|
|
|
|
// ── Supabase public config ────────────────────────────────────────────────
|
|
// CORS-gated: only requests from neurontechnologies.ai origins or localhost
|
|
// may fetch the anon key. Restricting this reduces the blast radius of any
|
|
// future Supabase RLS misconfiguration — an attacker on an arbitrary origin
|
|
// would not be able to silently obtain the key to make authenticated calls.
|
|
if str_eq(path, "/api/supabase-config") {
|
|
let req_origin: String = map_get(headers, "origin")
|
|
// map_get returns 0 (null) when the header is absent — same-origin
|
|
// browser fetches don't send Origin at all. str_starts_with(null, "http")
|
|
// returns false, so !origin_present correctly passes no-origin requests.
|
|
let origin_present: Bool = str_starts_with(req_origin, "http")
|
|
let origin_ok: Bool = !origin_present
|
|
|| str_eq(req_origin, "https://neurontechnologies.ai")
|
|
|| str_eq(req_origin, "https://www.neurontechnologies.ai")
|
|
|| str_starts_with(req_origin, "http://localhost:")
|
|
|| str_starts_with(req_origin, "http://127.0.0.1:")
|
|
|| str_starts_with(req_origin, "https://marketing-stage-")
|
|
if !origin_ok {
|
|
return "{\"__status__\":403,\"error\":\"forbidden\"}"
|
|
}
|
|
let proj_url: String = "https://ocojsghaonltunidkzpw.supabase.co"
|
|
let anon_key: String = state_get("__supabase_anon_key__")
|
|
return "{\"url\":\"" + proj_url + "\",\"anon_key\":\"" + anon_key + "\"}"
|
|
}
|
|
|
|
// ── Soul health diagnostic — surfaces raw signal from in-container soul ──
|
|
// Requires X-Internal: true header to prevent public exposure of internal
|
|
// service topology, soul URL, and probe responses.
|
|
if str_eq(path, "/api/soul-health") {
|
|
let x_internal: String = map_get(headers, "x-internal")
|
|
if !str_eq(x_internal, "true") {
|
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
|
}
|
|
if str_eq(method, "GET") {
|
|
let soul_base: String = state_get("__soul_url__")
|
|
// Probe 1: bare GET / — does ANYTHING listen?
|
|
let probe_root: String = http_get(soul_base + "/")
|
|
let probe_root_len: Int = str_len(probe_root)
|
|
// Probe 2: /healthz if soul has one
|
|
let probe_health: String = http_get(soul_base + "/healthz")
|
|
let probe_health_len: Int = str_len(probe_health)
|
|
// Probe 3: empty POST to /dharma/recv — does that route exist?
|
|
let probe_dharma_empty: String = http_post(soul_base + "/dharma/recv", "")
|
|
let probe_dharma_empty_len: Int = str_len(probe_dharma_empty)
|
|
// Probe 4: minimal valid envelope to /dharma/recv
|
|
let probe_envelope: String = "{\"channel\":\"health-probe\",\"from\":\"web-tier\",\"content\":\"{}\"}"
|
|
let probe_dharma: String = http_post(soul_base + "/dharma/recv", probe_envelope)
|
|
let probe_dharma_len: Int = str_len(probe_dharma)
|
|
// Escape body content for embedding in JSON response
|
|
let root_safe: String = str_replace(str_replace(probe_root, "\\", "\\\\"), "\"", "\\\"")
|
|
let health_safe: String = str_replace(str_replace(probe_health, "\\", "\\\\"), "\"", "\\\"")
|
|
let de_safe: String = str_replace(str_replace(probe_dharma_empty, "\\", "\\\\"), "\"", "\\\"")
|
|
let dharma_safe: String = str_replace(str_replace(probe_dharma, "\\", "\\\\"), "\"", "\\\"")
|
|
// Truncate via str_slice(s, 0, N); str_slice clamps end to len
|
|
let root_trunc: String = str_slice(root_safe, 0, 200)
|
|
let health_trunc: String = str_slice(health_safe, 0, 200)
|
|
let de_trunc: String = str_slice(de_safe, 0, 200)
|
|
let dharma_trunc: String = str_slice(dharma_safe, 0, 400)
|
|
return "{\"soul_url\":\"" + soul_base + "\",\"probe_root\":{\"len\":" + int_to_str(probe_root_len) + ",\"body_first_200\":\"" + root_trunc + "\"},\"probe_healthz\":{\"len\":" + int_to_str(probe_health_len) + ",\"body_first_200\":\"" + health_trunc + "\"},\"probe_dharma_empty\":{\"len\":" + int_to_str(probe_dharma_empty_len) + ",\"body_first_200\":\"" + de_trunc + "\"},\"probe_dharma_envelope\":{\"len\":" + int_to_str(probe_dharma_len) + ",\"body_first_400\":\"" + dharma_trunc + "\"}}"
|
|
}
|
|
}
|
|
|
|
// ── Demo chat — proxies to the demo soul at 7772 ─────────────────────────
|
|
if str_eq(path, "/api/demo") {
|
|
if str_eq(method, "POST") {
|
|
let msg: String = json_get(body, "message")
|
|
if str_eq(msg, "") {
|
|
return "{\"error\":\"message required\"}"
|
|
}
|
|
// Input length guard: ~2000 tokens ≈ 8000 characters
|
|
if str_len(msg) > 8000 {
|
|
return "{\"error\":\"Message too long. Please keep your message under 8000 characters.\"}"
|
|
}
|
|
// ── Kill switch — budget circuit breaker (Supabase demo_config) ──
|
|
// Polls demo_config.demo_enabled every 60s. Fails open on error so
|
|
// a Supabase hiccup does not break the demo for legitimate users.
|
|
let ks_sb_url: String = state_get("__supabase_project_url__")
|
|
let ks_sb_key: String = state_get("__supabase_service_key__")
|
|
let ks_now: Int = unix_timestamp()
|
|
let ks_checked_at: String = state_get("__demo_enabled_checked_at__")
|
|
let ks_checked_n: Int = if str_eq(ks_checked_at, "") { 0 } else { str_to_int(ks_checked_at) }
|
|
let ks_enabled: String = state_get("__demo_enabled_cache__")
|
|
// On first boot set defaults
|
|
if str_eq(ks_enabled, "") {
|
|
state_set("__demo_enabled_cache__", "true")
|
|
let ks_enabled = "true"
|
|
}
|
|
// Refresh cache if >60s old and service key is present
|
|
if (ks_now - ks_checked_n) > 60 && !str_eq(ks_sb_key, "") {
|
|
let ks_resp: String = supabase_get(ks_sb_url, ks_sb_key,
|
|
"demo_config?key=eq.demo_enabled&select=value&limit=1")
|
|
let ks_row: String = json_array_get(ks_resp, 0)
|
|
if !str_eq(ks_row, "") {
|
|
let ks_val: String = json_get(ks_row, "value")
|
|
if !str_eq(ks_val, "") {
|
|
state_set("__demo_enabled_cache__", ks_val)
|
|
let ks_enabled = ks_val
|
|
}
|
|
}
|
|
state_set("__demo_enabled_checked_at__", int_to_str(ks_now))
|
|
}
|
|
if str_eq(ks_enabled, "false") {
|
|
return "{\"error\":\"The demo is temporarily unavailable. Check back soon.\",\"disabled\":true}"
|
|
}
|
|
// ── Global circuit breaker ────────────────────────────────────────
|
|
// Caps total demo requests per Cloud Run instance per UTC day to 2000.
|
|
// This bounds per-instance API spend regardless of uid diversity.
|
|
// Stored in process state (in-memory) — intentionally per-instance
|
|
// so no cross-instance coordination is needed for this coarse cap.
|
|
let now_ts_cb: Int = unix_timestamp()
|
|
let today_day_cb: Int = now_ts_cb / 86400
|
|
let global_day_s: String = state_get("__global_demo_day__")
|
|
let global_cnt_s: String = state_get("__global_demo_count__")
|
|
let global_day: Int = if str_eq(global_day_s, "") { 0 } else { str_to_int(global_day_s) }
|
|
let global_cnt: Int = if str_eq(global_cnt_s, "") { 0 } else { str_to_int(global_cnt_s) }
|
|
// Reset on new UTC day
|
|
if global_day != today_day_cb {
|
|
state_set("__global_demo_day__", int_to_str(today_day_cb))
|
|
state_set("__global_demo_count__", "0")
|
|
let global_cnt = 0
|
|
}
|
|
if global_cnt >= 2000 {
|
|
return "{\"error\":\"Demo is temporarily busy. Try again in a few minutes.\",\"busy\":true}"
|
|
}
|
|
state_set("__global_demo_count__", int_to_str(global_cnt + 1))
|
|
|
|
// ── Auth: verify Supabase access_token ────────────────────────────
|
|
// The widget sends an access_token from the signed-in Supabase session.
|
|
// Verify it against the Supabase auth API to get the verified user ID.
|
|
// Reject unauthenticated requests outright.
|
|
let access_token: String = json_get(body, "access_token")
|
|
let auth_sb_url: String = state_get("__supabase_project_url__")
|
|
let auth_anon: String = state_get("__supabase_anon_key__")
|
|
let verified_uid: String = ""
|
|
if str_eq(access_token, "") {
|
|
return "{\"error\":\"Sign in required to use the demo.\",\"auth_required\":true}"
|
|
}
|
|
// supabase_auth_user calls GET /auth/v1/user with both Authorization
|
|
// (user's Bearer token) and apikey (anon key) headers.
|
|
let auth_resp: String = supabase_auth_user(auth_sb_url, auth_anon, access_token)
|
|
let auth_uid: String = json_get(auth_resp, "id")
|
|
if str_eq(auth_uid, "") {
|
|
return "{\"error\":\"Sign in required to use the demo.\",\"auth_required\":true}"
|
|
}
|
|
let verified_uid = auth_uid
|
|
|
|
// ── Per-uid rate limit (Supabase — shared across all instances) ───
|
|
// Uses demo_rate_limits table: uid (PK), count, day_number, updated_at.
|
|
// Falls back to in-process state_get/state_set when the service key is
|
|
// absent (local dev without SUPABASE_SERVICE_KEY set).
|
|
// Returns rate_limited JSON with reset_at (next midnight UTC) so
|
|
// the frontend can show a real countdown.
|
|
let rate_uid: String = verified_uid
|
|
let now_ts: Int = unix_timestamp()
|
|
let today_day: Int = now_ts / 86400
|
|
let next_reset: Int = (today_day + 1) * 86400
|
|
if !str_eq(rate_uid, "") {
|
|
let rl_sb_url: String = state_get("__supabase_project_url__")
|
|
let rl_sb_key: String = state_get("__supabase_service_key__")
|
|
if str_eq(rl_sb_key, "") {
|
|
// Local dev fallback: in-process rate limiting
|
|
let rl_key: String = "__rl_" + rate_uid
|
|
let rl_val: String = state_get(rl_key)
|
|
let rl_count: Int = 0
|
|
let rl_day: Int = 0
|
|
if !str_eq(rl_val, "") {
|
|
let parts: [String] = str_split(rl_val, "|")
|
|
if native_list_len(parts) >= 2 {
|
|
let rl_count = str_to_int(native_list_get(parts, 0))
|
|
let rl_day = str_to_int(native_list_get(parts, 1))
|
|
}
|
|
}
|
|
if rl_day != today_day {
|
|
let rl_count = 0
|
|
}
|
|
if rl_count >= 10 {
|
|
return "{\"rate_limited\":true,\"reset_at\":" + int_to_str(next_reset) + "}"
|
|
}
|
|
state_set(rl_key, int_to_str(rl_count + 1) + "|" + int_to_str(today_day))
|
|
} else {
|
|
// Production: read current count from Supabase
|
|
let rl_resp: String = supabase_get(rl_sb_url, rl_sb_key,
|
|
"demo_rate_limits?uid=eq." + rate_uid + "&select=count,day_number&limit=1")
|
|
let rl_row: String = json_array_get(rl_resp, 0)
|
|
let rl_count: Int = 0
|
|
let rl_day: Int = 0
|
|
if !str_eq(rl_row, "") {
|
|
let rl_count_s: String = json_get(rl_row, "count")
|
|
let rl_day_s: String = json_get(rl_row, "day_number")
|
|
if !str_eq(rl_count_s, "") {
|
|
let rl_count = str_to_int(rl_count_s)
|
|
}
|
|
if !str_eq(rl_day_s, "") {
|
|
let rl_day = str_to_int(rl_day_s)
|
|
}
|
|
}
|
|
// Reset count on new UTC day
|
|
if rl_day != today_day {
|
|
let rl_count = 0
|
|
}
|
|
if rl_count >= 10 {
|
|
return "{\"rate_limited\":true,\"reset_at\":" + int_to_str(next_reset) + "}"
|
|
}
|
|
// Upsert new count — supabase_insert uses Prefer: resolution=merge-duplicates
|
|
let new_count: Int = rl_count + 1
|
|
let rl_row_json: String = "{\"uid\":\"" + rate_uid
|
|
+ "\",\"count\":" + int_to_str(new_count)
|
|
+ ",\"day_number\":" + int_to_str(today_day) + "}"
|
|
let _rl_upsert: String = supabase_insert(rl_sb_url, rl_sb_key, "demo_rate_limits", rl_row_json)
|
|
}
|
|
}
|
|
// Turnstile: server-side verification is mandatory on every first
|
|
// message (tokens are single-use; per-message verification would
|
|
// break streaming chat flow so only the first message carries one).
|
|
// Requests without a cf_token are rejected outright — the widget
|
|
// must execute successfully before the first POST is sent.
|
|
let cf_token: String = json_get(body, "cf_token")
|
|
let ts_secret: String = state_get("__turnstile_secret_key__")
|
|
if str_eq(cf_token, "") && !str_eq(ts_secret, "") {
|
|
return "{\"error\":\"Bot check required. Please complete the challenge.\"}"
|
|
}
|
|
if !str_eq(cf_token, "") && !str_eq(ts_secret, "") {
|
|
let verify_body: String = "secret=" + ts_secret + "&response=" + cf_token
|
|
let verify_resp: String = http_post("https://challenges.cloudflare.com/turnstile/v1/siteverify", verify_body)
|
|
let is_valid: String = json_get(verify_resp, "success")
|
|
if !str_eq(is_valid, "true") {
|
|
return "{\"error\":\"Bot check failed. Please try again.\"}"
|
|
}
|
|
}
|
|
// Per-user channel ID — prevents conversation bleed between users
|
|
let uid: String = json_get(body, "uid")
|
|
let channel: String = if str_eq(uid, "") { "ntn-demo" } else { "ntn-demo-" + uid }
|
|
// Escape the message for embedding in JSON strings
|
|
let msg_safe: String = str_replace(str_replace(msg, "\\", "\\\\"), "\"", "\\\"")
|
|
// Include conversation history so soul has full thread context
|
|
// even after restarts. history is a JSON array of {role, content} from browser.
|
|
let hist_raw: String = json_get_raw(body, "history")
|
|
let hist_safe: String = if str_eq(hist_raw, "") { "[]" } else { hist_raw }
|
|
// Include browser-local engram activation for this turn
|
|
let an_raw: String = json_get_raw(body, "an")
|
|
let ec_str: String = json_get(body, "ec")
|
|
let an_safe: String = if str_eq(an_raw, "") { "[]" } else { an_raw }
|
|
let ec_safe: String = if str_eq(ec_str, "") { "0" } else { ec_str }
|
|
// questions_remaining + is_last_question let the soul close
|
|
// the conversation in voice when the visitor is on their last
|
|
// turn instead of leaving them at a hard rate-limit wall.
|
|
let qrem_str: String = json_get(body, "questions_remaining")
|
|
let qrem_safe: String = if str_eq(qrem_str, "") { "10" } else { qrem_str }
|
|
let is_last_str: String = json_get(body, "is_last_question")
|
|
let is_last_safe: String = if str_eq(is_last_str, "true") { "true" } else { "false" }
|
|
let user_name_raw: String = json_get_string(body, "user_name")
|
|
let user_tz_raw: String = json_get_string(body, "user_timezone")
|
|
let tod_raw: String = json_get_string(body, "time_of_day")
|
|
let is_return_raw: String = json_get_string(body, "is_return")
|
|
let user_name_safe: String = str_replace(str_replace(user_name_raw, "\\", "\\\\"), "\"", "\\\"")
|
|
let user_tz_safe: String = str_replace(str_replace(user_tz_raw, "\\", "\\\\"), "\"", "\\\"")
|
|
let tod_safe: String = str_replace(str_replace(tod_raw, "\\", "\\\\"), "\"", "\\\"")
|
|
let is_return_safe: String = if str_eq(is_return_raw, "true") { "true" } else { "false" }
|
|
// Look up the configured chat model from public.neuron_config
|
|
// (Phase 1 runtime config store). 60s TTL caching, falls back
|
|
// to the hardcoded default on Supabase miss / error.
|
|
let configured_model: String = config_get("chat.model")
|
|
let model_safe: String = if str_eq(configured_model, "") { "claude-sonnet-4-5" } else { configured_model }
|
|
// Build inner content with history and engram context for thread context.
|
|
// soul-demo unwraps payload from the dharma envelope, then reads
|
|
// model with json_get(body, "model") - so this propagates end to end.
|
|
let inner: String = "{\"event_type\":\"chat\",\"payload\":{\"message\":\"" + msg_safe + "\",\"history\":" + hist_safe + ",\"an\":" + an_safe + ",\"ec\":" + ec_safe + ",\"questions_remaining\":" + qrem_safe + ",\"is_last_turn\":" + is_last_safe + ",\"model\":\"" + model_safe + "\",\"user_name\":\"" + user_name_safe + "\",\"user_timezone\":\"" + user_tz_safe + "\",\"time_of_day\":\"" + tod_safe + "\",\"is_return\":\"" + is_return_safe + "\"}}"
|
|
// Escape inner for the outer content field
|
|
let inner_safe: String = str_replace(str_replace(inner, "\\", "\\\\"), "\"", "\\\"")
|
|
// Build dharma envelope with per-user channel
|
|
let envelope: String = "{\"channel\":\"" + channel + "\",\"from\":\"ntn-site\",\"content\":\"" + inner_safe + "\"}"
|
|
let soul_endpoint: String = state_get("__soul_url__") + "/dharma/recv"
|
|
let resp: String = http_post(soul_endpoint, envelope)
|
|
if str_eq(resp, "") {
|
|
return "{\"response\":\"Stepped out for a moment. Try again.\"}"
|
|
}
|
|
// SECURITY: Strip internal fields before returning to browser.
|
|
// activation_nodes and context_nodes expose the full knowledge graph —
|
|
// internal architecture, roadmap, self-model, DHARMA details.
|
|
// sn and se are safe: they are derived from
|
|
// the user's own conversation and contain only content they already saw.
|
|
let response_text: String = json_get(resp, "response")
|
|
if str_eq(response_text, "") {
|
|
let err: String = json_get(resp, "error")
|
|
if !str_eq(err, "") {
|
|
return "{\"response\":\"Stepped out for a moment. Try again.\"}"
|
|
}
|
|
return "{\"response\":\"Stepped out for a moment. Try again.\"}"
|
|
}
|
|
let safe_text: String = str_replace(str_replace(str_replace(str_replace(response_text, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n"), "\r", "\\r")
|
|
let model: String = json_get(resp, "model")
|
|
let sn_raw: String = json_get_raw(resp, "sn")
|
|
let se_raw: String = json_get_raw(resp, "se")
|
|
let sn_safe: String = if str_eq(sn_raw, "") { "[]" } else { sn_raw }
|
|
let se_safe: String = if str_eq(se_raw, "") { "[]" } else { se_raw }
|
|
return "{\"response\":\"" + safe_text + "\",\"model\":\"" + model + "\",\"sn\":" + sn_safe + ",\"se\":" + se_safe + "}"
|
|
}
|
|
return "{\"error\":\"POST required\"}"
|
|
}
|
|
|
|
// ── Stripe webhook ────────────────────────────────────────────────────────
|
|
// Handles three event types:
|
|
// checkout.session.completed - legacy hosted-checkout flow
|
|
// payment_intent.succeeded - integrated Elements + charge-now
|
|
// setup_intent.succeeded - integrated Elements + hold-until-launch
|
|
//
|
|
// For every successful purchase the handler:
|
|
// 1. Bumps the founding counter (if plan=founding) and persists
|
|
// 2. Upserts the waitlist row (email-keyed)
|
|
// 3. Auto-provisions a Supabase account via /auth/v1/invite — sends a
|
|
// magic-link invite email so the buyer can sign in and see their
|
|
// plan on /account. Idempotent: existing users get a fresh link.
|
|
// 4. Forwards to license API for key provisioning when configured.
|
|
//
|
|
// SECURITY: Stripe-Signature header is verified via HMAC-SHA256 before
|
|
// any processing occurs. Without this check an attacker could POST a
|
|
// forged payment_intent.succeeded event and increment the founding counter
|
|
// or trigger account provisioning for an arbitrary email.
|
|
//
|
|
// Stripe signature format: "t=<timestamp>,v1=<hex_sig>[,v1=...]"
|
|
// Signed payload: "<timestamp>.<raw_body>"
|
|
// Key: STRIPE_WEBHOOK_SECRET (whsec_... value from Stripe dashboard)
|
|
if str_eq(path, "/api/webhooks/stripe") {
|
|
let wh_secret: String = state_get("__stripe_webhook_secret__")
|
|
if !str_eq(wh_secret, "") {
|
|
let sig_header: String = map_get(headers, "stripe-signature")
|
|
if str_eq(sig_header, "") {
|
|
println("[webhook] rejected: missing Stripe-Signature header")
|
|
return "{\"__status__\":400,\"error\":\"missing signature\"}"
|
|
}
|
|
// Extract t= value from sig header
|
|
let t_idx: Int = str_index_of(sig_header, "t=")
|
|
let t_val: String = ""
|
|
if t_idx >= 0 {
|
|
let t_tail: String = str_slice(sig_header, t_idx + 2, str_len(sig_header))
|
|
let t_comma: Int = str_index_of(t_tail, ",")
|
|
let t_val = if t_comma >= 0 { str_slice(t_tail, 0, t_comma) } else { t_tail }
|
|
}
|
|
// Extract v1= value from sig header
|
|
let v1_idx: Int = str_index_of(sig_header, "v1=")
|
|
let v1_val: String = ""
|
|
if v1_idx >= 0 {
|
|
let v1_tail: String = str_slice(sig_header, v1_idx + 3, str_len(sig_header))
|
|
let v1_comma: Int = str_index_of(v1_tail, ",")
|
|
let v1_val = if v1_comma >= 0 { str_slice(v1_tail, 0, v1_comma) } else { v1_tail }
|
|
}
|
|
if str_eq(t_val, "") || str_eq(v1_val, "") {
|
|
println("[webhook] rejected: malformed Stripe-Signature header")
|
|
return "{\"__status__\":400,\"error\":\"invalid signature format\"}"
|
|
}
|
|
// Compute expected HMAC: HMAC-SHA256(secret, "<t_val>.<body>")
|
|
let signed_payload: String = t_val + "." + body
|
|
let expected_sig: String = hmac_sha256_hex(wh_secret, signed_payload)
|
|
if !str_eq(expected_sig, v1_val) {
|
|
println("[webhook] rejected: signature mismatch")
|
|
return "{\"__status__\":400,\"error\":\"signature verification failed\"}"
|
|
}
|
|
}
|
|
let is_session_done: Bool = str_contains(body, "checkout.session.completed")
|
|
let is_pi_done: Bool = str_contains(body, "payment_intent.succeeded")
|
|
let is_si_done: Bool = str_contains(body, "setup_intent.succeeded")
|
|
|
|
if is_session_done || is_pi_done || is_si_done {
|
|
// Pull email/name/customer_id - fields differ slightly across event
|
|
// types, walk a few candidates.
|
|
let customer_email: String = json_get(body, "receipt_email")
|
|
if str_eq(customer_email, "") { let customer_email = json_get(body, "customer_details.email") }
|
|
if str_eq(customer_email, "") { let customer_email = json_get(body, "billing_details.email") }
|
|
let customer_name: String = json_get(body, "customer_details.name")
|
|
if str_eq(customer_name, "") { let customer_name = json_get(body, "billing_details.name") }
|
|
let customer_id: String = json_get(body, "customer")
|
|
|
|
// Plan inference from metadata
|
|
let plan: String = "free"
|
|
if str_contains(body, "\"plan\":\"founding\"") { plan = "founding" }
|
|
if str_contains(body, "\"plan\":\"professional\"") { plan = "professional" }
|
|
|
|
// 1. Founding counter bump (one-shot) + waitlist
|
|
if str_eq(plan, "founding") && (is_session_done || is_pi_done) {
|
|
let current_sold: Int = get_sold()
|
|
let new_sold: Int = current_sold + 1
|
|
state_set("__founding_sold__", int_to_str(new_sold))
|
|
persist_founding_count(new_sold)
|
|
println("[webhook] founding sold: " + int_to_str(new_sold))
|
|
if !str_eq(customer_email, "") {
|
|
waitlist_upsert(customer_email, customer_name, "founding", "stripe-purchase", "", "", new_sold)
|
|
}
|
|
}
|
|
if str_eq(plan, "professional") && !str_eq(customer_email, "") {
|
|
waitlist_upsert(customer_email, customer_name, "professional", "stripe-purchase", "", "", 0)
|
|
}
|
|
|
|
// 2. Stamp stripe_customer_id on the waitlist row
|
|
let wb_sb_url: String = state_get("__supabase_project_url__")
|
|
let wb_sb_key: String = state_get("__supabase_service_key__")
|
|
if !str_eq(customer_email, "") && !str_eq(customer_id, "") && !str_eq(wb_sb_key, "") {
|
|
let cid_safe: String = str_replace(customer_id, "\"", "\\\"")
|
|
let update_row: String = "{\"stripe_customer_id\":\"" + cid_safe + "\"}"
|
|
let _wl_resp: String = supabase_insert(wb_sb_url, wb_sb_key, "waitlist?email=eq." + customer_email + "&on_conflict=email,plan", update_row)
|
|
}
|
|
|
|
// 3. Auto-provision Supabase account + send magic-link invite
|
|
// so the buyer can sign in. Stripe Customer is updated with
|
|
// metadata.supabase_user_id so the cross-reference is durable
|
|
// even if the email changes later.
|
|
if !str_eq(customer_email, "") && !str_eq(wb_sb_url, "") && !str_eq(wb_sb_key, "") {
|
|
let email_safe: String = str_replace(customer_email, "\"", "\\\"")
|
|
let name_safe: String = str_replace(customer_name, "\"", "\\\"")
|
|
let plan_safe: String = str_replace(plan, "\"", "\\\"")
|
|
let cid_safe2: String = str_replace(customer_id, "\"", "\\\"")
|
|
let invite_body: String = "{\"email\":\"" + email_safe + "\""
|
|
+ ",\"data\":{\"name\":\"" + name_safe + "\""
|
|
+ ",\"plan\":\"" + plan_safe + "\""
|
|
+ ",\"stripe_customer_id\":\"" + cid_safe2 + "\"}"
|
|
+ ",\"redirect_to\":\"https://neurontechnologies.ai/account?welcome=1\"}"
|
|
let invite_resp: String = supabase_admin_invite(wb_sb_url, wb_sb_key, invite_body)
|
|
println("[webhook] supabase invite for " + customer_email + ": " + invite_resp)
|
|
// If the response includes a user id, propagate it back to
|
|
// the Stripe customer so future operations have the link.
|
|
let new_user_id: String = json_get(invite_resp, "id")
|
|
if !str_eq(new_user_id, "") && !str_eq(customer_id, "") {
|
|
let stripe_key: String = state_get("__stripe_secret_key__")
|
|
if !str_eq(stripe_key, "") {
|
|
let stripe_auth: String = "Bearer " + stripe_key
|
|
let cust_url: String = "https://api.stripe.com/v1/customers/" + customer_id
|
|
let cust_body: String = "metadata[supabase_user_id]=" + new_user_id
|
|
let _cust_resp: String = http_post_form_auth(cust_url, cust_body, stripe_auth)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Forward to license API for key provisioning
|
|
let license_api: String = state_get("__license_api_url__")
|
|
if !str_eq(license_api, "") {
|
|
let resp: String = http_post(license_api + "/api/v1/webhooks/stripe", body)
|
|
println("[webhook] forwarded to license API: " + resp)
|
|
}
|
|
}
|
|
return "{\"received\":true}"
|
|
}
|
|
|
|
// ── DocuSeal webhook — POST /api/docuseal/webhook/<token> ────────────────
|
|
// Path-token-authenticated receiver. Persists every event to Supabase
|
|
// docuseal_events (full body as jsonb) and emails Will via Resend on
|
|
// form.completed or form.declined. Token comes from DOCUSEAL_WEBHOOK_TOKEN
|
|
// env var (mounted from Secret Manager). No HMAC yet — DocuSeal does not
|
|
// currently expose a per-webhook signing secret.
|
|
if str_starts_with(path, "/api/docuseal/webhook/") {
|
|
if !str_eq(method, "POST") {
|
|
return "{\"error\":\"POST required\"}"
|
|
}
|
|
let token_in_path: String = str_slice(path, 22, str_len(path))
|
|
let expected_token: String = env("DOCUSEAL_WEBHOOK_TOKEN")
|
|
if str_eq(expected_token, "") || !str_eq(token_in_path, expected_token) {
|
|
return "{\"__status__\":401,\"error\":\"unauthorized\"}"
|
|
}
|
|
|
|
let event_type: String = json_get(body, "event_type")
|
|
let event_ts: String = json_get(body, "timestamp")
|
|
let data_raw: String = json_get_raw(body, "data")
|
|
let sub_id: String = if str_eq(data_raw, "") { "" } else { json_get(data_raw, "submission_id") }
|
|
let signer_email: String = if str_eq(data_raw, "") { "" } else { json_get(data_raw, "email") }
|
|
let signer_name: String = if str_eq(data_raw, "") { "" } else { json_get(data_raw, "name") }
|
|
let signer_ua: String = if str_eq(data_raw, "") { "" } else { json_get(data_raw, "ua") }
|
|
let signer_ip: String = if str_eq(data_raw, "") { "" } else { json_get(data_raw, "ip") }
|
|
println("[docuseal] event=" + event_type + " sub=" + sub_id + " email=" + signer_email)
|
|
|
|
let body_safe: String = str_replace(str_replace(str_replace(str_replace(body, "\\", "\\\\"), "\"", "\\\""), "\n", "\\n"), "\r", "\\r")
|
|
let name_safe: String = str_replace(str_replace(signer_name, "\\", "\\\\"), "\"", "\\\"")
|
|
let ua_safe: String = str_replace(str_replace(signer_ua, "\\", "\\\\"), "\"", "\\\"")
|
|
|
|
let sb_url: String = state_get("__supabase_project_url__")
|
|
let sb_key: String = state_get("__supabase_service_key__")
|
|
if !str_eq(sb_key, "") {
|
|
let sub_field: String = if str_eq(sub_id, "") { "null" } else { sub_id }
|
|
let email_field: String = if str_eq(signer_email, "") { "null" } else { "\"" + signer_email + "\"" }
|
|
let name_field: String = if str_eq(signer_name, "") { "null" } else { "\"" + name_safe + "\"" }
|
|
let ua_field: String = if str_eq(signer_ua, "") { "null" } else { "\"" + ua_safe + "\"" }
|
|
let ip_field: String = if str_eq(signer_ip, "") { "null" } else { "\"" + signer_ip + "\"" }
|
|
let ts_field: String = if str_eq(event_ts, "") { "null" } else { "\"" + event_ts + "\"" }
|
|
let row: String = "{\"event_type\":\"" + event_type
|
|
+ "\",\"event_timestamp\":" + ts_field
|
|
+ ",\"submission_id\":" + sub_field
|
|
+ ",\"signer_email\":" + email_field
|
|
+ ",\"signer_name\":" + name_field
|
|
+ ",\"ua\":" + ua_field
|
|
+ ",\"ip\":" + ip_field
|
|
+ ",\"payload\":\"" + body_safe + "\"}"
|
|
let ds_resp: String = supabase_insert(sb_url, sb_key, "docuseal_events", row)
|
|
println("[docuseal] event=" + event_type + " sub=" + sub_id + " -> " + ds_resp)
|
|
}
|
|
|
|
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 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}"
|
|
}
|
|
|
|
// ── Share card — POST /api/share ──────────────────────────────────────────
|
|
//
|
|
// Body: {question, answer, answer_html?, answer_plaintext?}
|
|
// - answer is the legacy plaintext field (still required for the
|
|
// gallery + DB row).
|
|
// - answer_html is optional pre-rendered (marked.js) HTML captured
|
|
// from bubble.innerHTML at Share-click time. When present, it
|
|
// replaces the escaped-plaintext bubble on the share card so the
|
|
// visual matches what the user saw in chat.
|
|
// - answer_plaintext, when supplied, takes precedence as the og:desc /
|
|
// gallery body. Falls back to `answer`.
|
|
//
|
|
// The handler sanitizes answer_html via the runtime allowlist sanitizer
|
|
// (el_html_sanitize): only the tags and attributes named in
|
|
// default_share_allowlist survive; everything else is dropped. Whole
|
|
// subtrees of script/style/iframe/object/embed/form are removed
|
|
// regardless of the allowlist; <a href> schemes are restricted to
|
|
// http/https/mailto/fragment/relative.
|
|
if str_eq(path, "/api/share") {
|
|
if !str_eq(method, "POST") {
|
|
return "{\"error\":\"POST required\"}"
|
|
}
|
|
let question: String = json_get(body, "question")
|
|
let answer: String = json_get(body, "answer")
|
|
let answer_html_raw: String = json_get(body, "answer_html")
|
|
let answer_plain_in: String = json_get(body, "answer_plaintext")
|
|
let answer_plain: String = if str_eq(answer_plain_in, "") { answer } else { answer_plain_in }
|
|
if str_eq(question, "") || str_eq(answer_plain, "") {
|
|
return "{\"error\":\"question and answer required\"}"
|
|
}
|
|
let ts: String = int_to_str(unix_timestamp())
|
|
let id: String = str_slice(ts, str_len(ts) - 8, str_len(ts))
|
|
let html_share: String = share_card_page(question, answer_plain, answer_html_raw, id)
|
|
let gcs_bucket: String = env("GCS_SHARE_BUCKET")
|
|
if !str_eq(gcs_bucket, "") {
|
|
// GCS — durable across Cloud Run instances and restarts
|
|
let gcs_ok: String = gcs_write(gcs_bucket, "cards/" + id + ".html", html_share)
|
|
println("[share] gcs write cards/" + id + ".html -> " + gcs_ok)
|
|
} else {
|
|
// Local dev fallback
|
|
fs_write(src_dir + "/shares/" + id + ".html", html_share)
|
|
}
|
|
// Write to Supabase share_cards for the gallery + voting. We store
|
|
// the plaintext answer (gallery thumbnails read this column).
|
|
let sb_url: String = state_get("__supabase_project_url__")
|
|
let sb_key: String = state_get("__supabase_service_key__")
|
|
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")
|
|
// 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 { el_html_sanitize(answer_html_raw, default_share_allowlist) }
|
|
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)
|
|
}
|
|
return "{\"id\":\"" + id + "\"}"
|
|
}
|
|
|
|
// ── Vote on a share card — POST /api/vote (auth-gated) ───────────────────
|
|
//
|
|
// Body: {access_token: <supabase-jwt>, id: <share_id>, direction: "up"|"down"|"none"}
|
|
// "up" - upsert vote with direction up (idempotent)
|
|
// "down" - upsert vote with direction down (or change from up)
|
|
// "none" - undo (delete the user's vote for this card)
|
|
//
|
|
// The access_token is validated against /auth/v1/user; the resulting JWT
|
|
// (NOT the service key) is the Authorization header on the share_votes
|
|
// write so RLS policy auth.uid()=user_id treats the row as user-owned.
|
|
// share_cards.upvotes/downvotes/score are kept in sync by the
|
|
// recalc_share_card_score trigger. After the write we re-read the
|
|
// aggregate with the service key (public read) so the client gets fresh
|
|
// totals + the new user_vote in one round trip.
|
|
if str_eq(path, "/api/vote") {
|
|
if !str_eq(method, "POST") {
|
|
return "{\"__status__\":405,\"error\":\"POST required\"}"
|
|
}
|
|
let v_jwt: String = json_get(body, "access_token")
|
|
let v_id: String = json_get(body, "id")
|
|
let v_dir: String = json_get(body, "direction")
|
|
if str_eq(v_id, "") || str_eq(v_dir, "") {
|
|
return "{\"__status__\":400,\"error\":\"id and direction required\"}"
|
|
}
|
|
if !str_eq(v_dir, "up") && !str_eq(v_dir, "down") && !str_eq(v_dir, "none") {
|
|
return "{\"__status__\":400,\"error\":\"direction must be up, down, or none\"}"
|
|
}
|
|
if str_eq(v_jwt, "") {
|
|
return "{\"__status__\":401,\"error\":\"login_required\"}"
|
|
}
|
|
let v_sb_url: String = state_get("__supabase_project_url__")
|
|
let v_anon: String = state_get("__supabase_anon_key__")
|
|
let v_service: String = state_get("__supabase_service_key__")
|
|
if str_eq(v_anon, "") || str_eq(v_service, "") {
|
|
return "{\"__status__\":500,\"error\":\"not_configured\"}"
|
|
}
|
|
// Validate the JWT - "" means invalid / expired / revoked.
|
|
let v_user: String = supabase_auth_user(v_sb_url, v_anon, v_jwt)
|
|
let v_uid: String = json_get(v_user, "id")
|
|
if str_eq(v_uid, "") {
|
|
return "{\"__status__\":401,\"error\":\"invalid_token\"}"
|
|
}
|
|
// Direction "none" - undo. DELETE share_votes where (share_id, user_id).
|
|
// The user JWT (NOT service key) is the auth so RLS auth.uid()=user_id
|
|
// passes. The recalc trigger updates share_cards aggregates.
|
|
if str_eq(v_dir, "none") {
|
|
let del_url: String = v_sb_url + "/rest/v1/share_votes?share_id=eq." + v_id + "&user_id=eq." + v_uid
|
|
let _del_resp: String = http_delete_auth(del_url, v_jwt, v_anon)
|
|
} else {
|
|
// up/down — delete any existing vote first (idempotent), then insert fresh.
|
|
// Upsert via user JWT requires both INSERT + UPDATE RLS policies; the
|
|
// UPDATE policy is often absent. Delete-then-insert is more reliable:
|
|
// user JWT covers the DELETE (auth.uid()=user_id RLS passes),
|
|
// service key covers the INSERT (bypasses RLS; user identity already
|
|
// validated above via supabase_auth_user). The recalc trigger fires on
|
|
// both operations and keeps share_cards.score accurate.
|
|
let del_url: String = v_sb_url + "/rest/v1/share_votes?share_id=eq." + v_id + "&user_id=eq." + v_uid
|
|
let _del_r: String = http_delete_auth(del_url, v_jwt, v_anon)
|
|
let row: String = "{\"share_id\":\"" + v_id + "\",\"user_id\":\"" + v_uid + "\",\"direction\":\"" + v_dir + "\"}"
|
|
let _ins_r: String = supabase_insert(v_sb_url, v_service, "share_votes", row)
|
|
}
|
|
// Re-fetch fresh aggregate from share_cards (service key - public read).
|
|
// PostgREST returns a JSON array; use json_array_get(0) then json_get.
|
|
let v_agg: String = supabase_get(
|
|
v_sb_url, v_service,
|
|
"share_cards?select=score,upvotes,downvotes&id=eq." + v_id
|
|
)
|
|
let v_row: String = json_array_get(v_agg, 0)
|
|
let v_score_raw: String = json_get(v_row, "score")
|
|
let v_up_raw: String = json_get(v_row, "upvotes")
|
|
let v_down_raw: String = json_get(v_row, "downvotes")
|
|
let v_score_str: String = if str_eq(v_score_raw, "") { "0" } else { v_score_raw }
|
|
let v_up_str: String = if str_eq(v_up_raw, "") { "0" } else { v_up_raw }
|
|
let v_down_str: String = if str_eq(v_down_raw, "") { "0" } else { v_down_raw }
|
|
let v_user_vote: String = if str_eq(v_dir, "none") { "none" } else { v_dir }
|
|
return "{\"ok\":true,\"score\":" + v_score_str + ",\"upvotes\":" + v_up_str + ",\"downvotes\":" + v_down_str + ",\"user_vote\":\"" + v_user_vote + "\"}"
|
|
}
|
|
|
|
// ── Vote state — GET /api/vote-state/<share_id> (auth-aware) ─────────────
|
|
//
|
|
// Always returns the public aggregate. If a Supabase access_token is
|
|
// supplied via ?access_token=<jwt>, validate it and include the user's
|
|
// current vote as `user_vote` (one of up / down / none). An anonymous
|
|
// caller gets `user_vote:"none"`.
|
|
if str_starts_with(path, "/api/vote-state/") {
|
|
let vs_rest: String = str_slice(path, 16, str_len(path))
|
|
// Strip query string from id if access_token was passed inline
|
|
let vs_q_idx: Int = str_index_of(vs_rest, "?")
|
|
let vs_id: String = if vs_q_idx >= 0 { str_slice(vs_rest, 0, vs_q_idx) } else { vs_rest }
|
|
// Pull access_token out of the query string
|
|
let vs_jwt: String = ""
|
|
let vs_at_idx: Int = str_index_of(vs_rest, "access_token=")
|
|
if vs_at_idx >= 0 {
|
|
let vs_tail: String = str_slice(vs_rest, vs_at_idx + 13, str_len(vs_rest))
|
|
let vs_amp_idx: Int = str_index_of(vs_tail, "&")
|
|
let vs_jwt = if vs_amp_idx >= 0 { str_slice(vs_tail, 0, vs_amp_idx) } else { vs_tail }
|
|
}
|
|
let vs_sb_url: String = state_get("__supabase_project_url__")
|
|
let vs_anon: String = state_get("__supabase_anon_key__")
|
|
let vs_service: String = state_get("__supabase_service_key__")
|
|
if str_eq(vs_service, "") {
|
|
return "{\"score\":0,\"upvotes\":0,\"downvotes\":0,\"user_vote\":\"none\"}"
|
|
}
|
|
let vs_resp: String = supabase_get(
|
|
vs_sb_url, vs_service,
|
|
"share_cards?select=score,upvotes,downvotes&id=eq." + vs_id
|
|
)
|
|
let vs_row: String = json_array_get(vs_resp, 0)
|
|
let vs_score_raw: String = json_get(vs_row, "score")
|
|
let vs_up_raw: String = json_get(vs_row, "upvotes")
|
|
let vs_down_raw: String = json_get(vs_row, "downvotes")
|
|
let vs_score_str: String = if str_eq(vs_score_raw, "") { "0" } else { vs_score_raw }
|
|
let vs_up_str: String = if str_eq(vs_up_raw, "") { "0" } else { vs_up_raw }
|
|
let vs_down_str: String = if str_eq(vs_down_raw, "") { "0" } else { vs_down_raw }
|
|
let vs_user_vote: String = "none"
|
|
if !str_eq(vs_jwt, "") {
|
|
let vs_user: String = supabase_auth_user(vs_sb_url, vs_anon, vs_jwt)
|
|
let vs_uid: String = json_get(vs_user, "id")
|
|
if !str_eq(vs_uid, "") {
|
|
let vs_vote_resp: String = supabase_get(
|
|
vs_sb_url, vs_service,
|
|
"share_votes?select=direction&share_id=eq." + vs_id + "&user_id=eq." + vs_uid
|
|
)
|
|
let vs_vote_row: String = json_array_get(vs_vote_resp, 0)
|
|
let vs_vote_dir: String = json_get(vs_vote_row, "direction")
|
|
let vs_user_vote = if str_eq(vs_vote_dir, "") { "none" } else { vs_vote_dir }
|
|
}
|
|
}
|
|
return "{\"score\":" + vs_score_str + ",\"upvotes\":" + vs_up_str + ",\"downvotes\":" + vs_down_str + ",\"user_vote\":\"" + vs_user_vote + "\"}"
|
|
}
|
|
|
|
// ── Vote count — GET /api/vote-count/<id> (legacy alias) ─────────────────
|
|
// Kept for any cached HTML still polling the old endpoint shape. Returns
|
|
// aggregate only - no user_vote. New clients should use /api/vote-state.
|
|
if str_starts_with(path, "/api/vote-count/") {
|
|
let vc_id: String = str_slice(path, 16, str_len(path))
|
|
let vc_sb_url: String = state_get("__supabase_project_url__")
|
|
let vc_sb_key: String = state_get("__supabase_service_key__")
|
|
if str_eq(vc_sb_key, "") {
|
|
return "{\"score\":0,\"upvotes\":0,\"downvotes\":0}"
|
|
}
|
|
let vc_resp: String = supabase_get(
|
|
vc_sb_url, vc_sb_key,
|
|
"share_cards?select=score,upvotes,downvotes&id=eq." + vc_id
|
|
)
|
|
let vc_row: String = json_array_get(vc_resp, 0)
|
|
let vc_score_raw: String = json_get(vc_row, "score")
|
|
let vc_up_raw: String = json_get(vc_row, "upvotes")
|
|
let vc_down_raw: String = json_get(vc_row, "downvotes")
|
|
let vc_score_str: String = if str_eq(vc_score_raw, "") { "0" } else { vc_score_raw }
|
|
let vc_up_str: String = if str_eq(vc_up_raw, "") { "0" } else { vc_up_raw }
|
|
let vc_down_str: String = if str_eq(vc_down_raw, "") { "0" } else { vc_down_raw }
|
|
return "{\"score\":" + vc_score_str + ",\"upvotes\":" + vc_up_str + ",\"downvotes\":" + vc_down_str + "}"
|
|
}
|
|
|
|
// ── Gallery — GET /said ───────────────────────────────────────────────────
|
|
//
|
|
// Renders the gallery server-side and injects the Supabase config so the
|
|
// page's vote JS can call supabase.auth.getSession() and POST /api/vote
|
|
// with the user's JWT.
|
|
if str_eq(path, "/said") {
|
|
let sb_url: String = state_get("__supabase_project_url__")
|
|
let sb_key: String = state_get("__supabase_service_key__")
|
|
let sb_anon: String = state_get("__supabase_anon_key__")
|
|
let cards_json: String = ""
|
|
if !str_eq(sb_key, "") {
|
|
let cards_json = supabase_get(
|
|
sb_url,
|
|
sb_key,
|
|
"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)
|
|
}
|
|
|
|
// ── Share card — GET /share/<id> ──────────────────────────────────────────
|
|
if str_starts_with(path, "/share/") {
|
|
let id: String = str_slice(path, 7, str_len(path))
|
|
let share_html: String = ""
|
|
let gcs_bucket2: String = env("GCS_SHARE_BUCKET")
|
|
if !str_eq(gcs_bucket2, "") {
|
|
let share_html = gcs_read(gcs_bucket2, "cards/" + id + ".html")
|
|
} else {
|
|
let share_html = fs_read(src_dir + "/shares/" + id + ".html")
|
|
}
|
|
// Guard against empty responses and GCS error JSON (e.g. {"error":...}).
|
|
// A valid share card always starts with "<" (HTML). Anything else is
|
|
// treated as a missing card to avoid leaking bucket names or GCS details.
|
|
if str_eq(share_html, "") {
|
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
|
}
|
|
if !str_starts_with(share_html, "<") {
|
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
|
}
|
|
return share_html
|
|
}
|
|
|
|
// ── Checkout success ──────────────────────────────────────────────────────
|
|
// Stripe appends ?payment_intent=...&payment_intent_client_secret=...&redirect_status=succeeded
|
|
// to the return_url. Use prefix match so the query string does not 404.
|
|
if str_starts_with(path, "/marketplace/success") {
|
|
let badge_html: String = founding_badge(get_sold())
|
|
let badge_css: String = founding_badge_css()
|
|
let success_body: String = el_div(
|
|
"style=\"min-height:80vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:4rem 2rem\"",
|
|
el_p("class=\"label\" style=\"margin-bottom:1.5rem\"", "You're in.")
|
|
+ el_h1("class=\"display-lg\" style=\"margin-bottom:1.25rem\"", "Welcome to Neuron.")
|
|
+ el_p(
|
|
"style=\"font-family:var(--body);font-weight:300;font-size:1.1rem;color:var(--t2);max-width:28rem;line-height:1.7;margin-bottom:3rem\"",
|
|
"Your license is being provisioned. Check your email - your license key and download instructions will be there in the next few minutes."
|
|
)
|
|
+ el_div("style=\"margin-bottom:3rem\"", badge_html)
|
|
+ el_div(
|
|
"style=\"display:flex;gap:1rem;flex-wrap:wrap;justify-content:center\"",
|
|
el_a("/account", "class=\"btn-primary\"", "View your account →")
|
|
+ el_a("/", "class=\"btn-ghost\"", "Back to home")
|
|
)
|
|
)
|
|
return page_open_seo(
|
|
"Welcome to Neuron — Your Membership is Confirmed",
|
|
"Your Neuron membership is confirmed. Download the app and let the AI that remembers you get to work.",
|
|
"/marketplace/success",
|
|
"Your Neuron membership is confirmed. Download the app and let the AI that remembers you get to work.",
|
|
"true"
|
|
) + badge_css + success_body + page_close()
|
|
}
|
|
|
|
// ── Account dashboard ─────────────────────────────────────────────────────
|
|
// Use prefix match so OAuth/email-confirmation redirects with query strings still hit.
|
|
if str_starts_with(path, "/account") {
|
|
let sb_url: String = state_get("__supabase_project_url__")
|
|
let sb_anon: String = state_get("__supabase_anon_key__")
|
|
return account_page(sb_url, sb_anon)
|
|
}
|
|
|
|
// ── Founding badge fragment ───────────────────────────────────────────────
|
|
if str_starts_with(path, "/api/founding-badge") {
|
|
let n_str: String = "1"
|
|
let q: Int = str_index_of(path, "n=")
|
|
if q >= 0 {
|
|
let n_str = str_slice(path, q + 2, str_len(path))
|
|
}
|
|
let badge_html: String = founding_badge(str_to_int(n_str))
|
|
let badge_css: String = founding_badge_css()
|
|
return badge_css + badge_html
|
|
}
|
|
|
|
// ── Family plan — POST /api/family/invite ────────────────────────────────
|
|
if str_eq(path, "/api/family/invite") {
|
|
if !str_eq(method, "POST") {
|
|
return "{\"error\":\"POST required\"}"
|
|
}
|
|
let parent_email: String = json_get(body, "parent_email")
|
|
let child_email: String = json_get(body, "child_email")
|
|
let child_dob_year_str: String = json_get(body, "child_dob_year")
|
|
let attested_str: String = json_get(body, "attested")
|
|
if str_eq(parent_email, "") || str_eq(child_email, "") || str_eq(child_dob_year_str, "") {
|
|
return "{\"error\":\"parent_email, child_email, and child_dob_year are required\"}"
|
|
}
|
|
if !str_eq(attested_str, "true") {
|
|
return "{\"error\":\"attestation required\"}"
|
|
}
|
|
let child_dob_year: Int = str_to_int(child_dob_year_str)
|
|
if child_dob_year < 2008 {
|
|
return "{\"error\":\"Child must be under 18. Birth year must be 2008 or later.\"}"
|
|
}
|
|
// Check existing family member count
|
|
let fam_sb_url: String = state_get("__supabase_project_url__")
|
|
let fam_sb_key: String = state_get("__supabase_service_key__")
|
|
if str_eq(fam_sb_key, "") {
|
|
return "{\"error\":\"not configured\"}"
|
|
}
|
|
let pe_safe: String = str_replace(str_replace(parent_email, "\\", "\\\\"), "\"", "\\\"")
|
|
let ce_safe: String = str_replace(str_replace(child_email, "\\", "\\\\"), "\"", "\\\"")
|
|
let members_resp: String = http_get_auth(
|
|
fam_sb_url + "/rest/v1/family_members?parent_email=eq." + parent_email + "&status=neq.cancelled&select=child_email",
|
|
fam_sb_key
|
|
)
|
|
let member_count: Int = json_array_len(members_resp)
|
|
if member_count >= 5 {
|
|
return "{\"error\":\"Maximum of 5 family members reached.\"}"
|
|
}
|
|
// Check child not already added
|
|
let dup_resp: String = http_get_auth(
|
|
fam_sb_url + "/rest/v1/family_members?parent_email=eq." + parent_email + "&child_email=eq." + child_email + "&select=child_email",
|
|
fam_sb_key
|
|
)
|
|
let dup_count: Int = json_array_len(dup_resp)
|
|
if dup_count > 0 {
|
|
return "{\"error\":\"This child email is already in your family plan.\"}"
|
|
}
|
|
// Create Stripe customer for child
|
|
let fam_stripe_key: String = state_get("__stripe_secret_key__")
|
|
if str_eq(fam_stripe_key, "") {
|
|
return "{\"error\":\"Stripe not configured\"}"
|
|
}
|
|
let child_email_safe: String = str_replace(child_email, "@", "%40")
|
|
let customer_body: String = "email=" + child_email_safe
|
|
let customer_resp: String = http_post_auth("https://api.stripe.com/v1/customers", fam_stripe_key, customer_body)
|
|
let child_customer_id: String = json_get(customer_resp, "id")
|
|
if str_eq(child_customer_id, "") {
|
|
return "{\"error\":\"Failed to create Stripe customer\"}"
|
|
}
|
|
// Create Stripe subscription for child
|
|
let price_id: String = state_get("__stripe_price_family_child__")
|
|
if str_eq(price_id, "") {
|
|
return "{\"error\":\"Family child price not configured\"}"
|
|
}
|
|
let parent_email_safe: String = str_replace(parent_email, "@", "%40")
|
|
let sub_body: String = "customer=" + child_customer_id
|
|
+ "&items[0][price]=" + price_id
|
|
+ "&trial_period_days=7"
|
|
+ "&metadata[parent_email]=" + parent_email_safe
|
|
+ "&metadata[type]=family_child"
|
|
let sub_resp: String = http_post_auth("https://api.stripe.com/v1/subscriptions", fam_stripe_key, sub_body)
|
|
let sub_id: String = json_get(sub_resp, "id")
|
|
if str_eq(sub_id, "") {
|
|
return "{\"error\":\"Failed to create Stripe subscription\"}"
|
|
}
|
|
// Generate invite token
|
|
let ts_str: String = int_to_str(unix_timestamp())
|
|
let email_prefix: String = str_slice(child_email, 0, 4)
|
|
let invite_token: String = ts_str + email_prefix
|
|
// Insert into family_members table
|
|
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
|
|
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 + "\"}"
|
|
}
|
|
|
|
// ── Family plan — GET /api/family/members ────────────────────────────────
|
|
if str_starts_with(path, "/api/family/members") {
|
|
let fml_sb_url: String = state_get("__supabase_project_url__")
|
|
let fml_sb_key: String = state_get("__supabase_service_key__")
|
|
if str_eq(fml_sb_key, "") {
|
|
return "[]"
|
|
}
|
|
// Extract parent_email from query string
|
|
let fml_parent_email: String = ""
|
|
let q_idx: Int = str_index_of(path, "parent_email=")
|
|
if q_idx >= 0 {
|
|
let fml_parent_email = str_slice(path, q_idx + 13, str_len(path))
|
|
}
|
|
if str_eq(fml_parent_email, "") {
|
|
return "{\"error\":\"parent_email required\"}"
|
|
}
|
|
let fml_resp: String = http_get_auth(
|
|
fml_sb_url + "/rest/v1/family_members?parent_email=eq." + fml_parent_email + "&select=child_email,status,child_dob_year,created_at",
|
|
fml_sb_key
|
|
)
|
|
if str_eq(fml_resp, "") {
|
|
return "[]"
|
|
}
|
|
return fml_resp
|
|
}
|
|
|
|
// ── Family plan — POST /api/family/remove ────────────────────────────────
|
|
if str_eq(path, "/api/family/remove") {
|
|
if !str_eq(method, "POST") {
|
|
return "{\"error\":\"POST required\"}"
|
|
}
|
|
let rem_parent_email: String = json_get(body, "parent_email")
|
|
let rem_child_email: String = json_get(body, "child_email")
|
|
if str_eq(rem_parent_email, "") || str_eq(rem_child_email, "") {
|
|
return "{\"error\":\"parent_email and child_email required\"}"
|
|
}
|
|
let rem_sb_url: String = state_get("__supabase_project_url__")
|
|
let rem_sb_key: String = state_get("__supabase_service_key__")
|
|
if str_eq(rem_sb_key, "") {
|
|
return "{\"error\":\"not configured\"}"
|
|
}
|
|
// Fetch the subscription ID
|
|
let sub_lookup: String = http_get_auth(
|
|
rem_sb_url + "/rest/v1/family_members?parent_email=eq." + rem_parent_email + "&child_email=eq." + rem_child_email + "&select=stripe_subscription_id",
|
|
rem_sb_key
|
|
)
|
|
let rem_sub_id: String = json_get(sub_lookup, "0.stripe_subscription_id")
|
|
// Cancel Stripe subscription if present
|
|
if !str_eq(rem_sub_id, "") {
|
|
let rem_stripe_key: String = state_get("__stripe_secret_key__")
|
|
if !str_eq(rem_stripe_key, "") {
|
|
let cancel_resp: String = http_post_auth("https://api.stripe.com/v1/subscriptions/" + rem_sub_id + "/cancel", rem_stripe_key, "")
|
|
println("[family/remove] cancel sub " + rem_sub_id + " -> " + cancel_resp)
|
|
}
|
|
}
|
|
// Update status to cancelled
|
|
let rpe_safe: String = str_replace(str_replace(rem_parent_email, "\\", "\\\\"), "\"", "\\\"")
|
|
let rce_safe: String = str_replace(str_replace(rem_child_email, "\\", "\\\\"), "\"", "\\\"")
|
|
let cancel_row: String = "{\"status\":\"cancelled\"}"
|
|
supabase_insert(rem_sb_url, rem_sb_key, "family_members?parent_email=eq." + rem_parent_email + "&child_email=eq." + rem_child_email, cancel_row)
|
|
println("[family/remove] cancelled " + rce_safe + " from " + rpe_safe)
|
|
return "{\"ok\":true}"
|
|
}
|
|
|
|
// ── API key provisioning — POST /api/api-keys ────────────────────────────
|
|
// Returns user's stored provider keys (masked) for display on /account.
|
|
// Body: { access_token: "<jwt>" }
|
|
if str_eq(path, "/api/api-keys") && str_eq(method, "POST") {
|
|
let ak_jwt: String = json_get_string(body, "access_token")
|
|
if str_eq(ak_jwt, "") {
|
|
return "{\"__status__\":401,\"error\":\"missing_jwt\"}"
|
|
}
|
|
let ak_url: String = state_get("__supabase_project_url__")
|
|
let ak_anon: String = state_get("__supabase_anon_key__")
|
|
let ak_service: String = state_get("__supabase_service_key__")
|
|
if str_eq(ak_url, "") {
|
|
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
|
|
}
|
|
let ak_user: String = supabase_auth_user(ak_url, ak_anon, ak_jwt)
|
|
let ak_uid: String = json_get(ak_user, "id")
|
|
if str_eq(ak_uid, "") {
|
|
return "{\"__status__\":401,\"error\":\"invalid_jwt\"}"
|
|
}
|
|
let ak_q: String = "user_api_keys?select=provider,key_value&user_id=eq." + ak_uid
|
|
let ak_rows: String = supabase_get(ak_url, ak_service, ak_q)
|
|
return "{\"rows\":" + ak_rows + "}"
|
|
}
|
|
|
|
// ── API key provisioning — POST /api/api-keys/save ───────────────────────
|
|
// Upserts a single provider key for the authenticated user.
|
|
// Body: { access_token: "<jwt>", provider: "openai"|"anthropic"|"gemini"|"grok", key: "<value>" }
|
|
if str_eq(path, "/api/api-keys/save") {
|
|
let aks_jwt: String = json_get_string(body, "access_token")
|
|
let aks_provider: String = json_get_string(body, "provider")
|
|
let aks_key: String = json_get_string(body, "key")
|
|
if str_eq(aks_jwt, "") {
|
|
return "{\"__status__\":401,\"error\":\"missing_jwt\"}"
|
|
}
|
|
if str_eq(aks_provider, "") || str_eq(aks_key, "") {
|
|
return "{\"__status__\":400,\"error\":\"missing_provider_or_key\"}"
|
|
}
|
|
let aks_valid_provider: Bool = str_eq(aks_provider, "openai")
|
|
|| str_eq(aks_provider, "anthropic")
|
|
|| str_eq(aks_provider, "gemini")
|
|
|| str_eq(aks_provider, "grok")
|
|
if !aks_valid_provider {
|
|
return "{\"__status__\":400,\"error\":\"invalid_provider\"}"
|
|
}
|
|
let aks_url: String = state_get("__supabase_project_url__")
|
|
let aks_anon: String = state_get("__supabase_anon_key__")
|
|
let aks_service: String = state_get("__supabase_service_key__")
|
|
if str_eq(aks_url, "") {
|
|
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
|
|
}
|
|
let aks_user: String = supabase_auth_user(aks_url, aks_anon, aks_jwt)
|
|
let aks_uid: String = json_get(aks_user, "id")
|
|
if str_eq(aks_uid, "") {
|
|
return "{\"__status__\":401,\"error\":\"invalid_jwt\"}"
|
|
}
|
|
let aks_row: String = "{\"user_id\":\"" + aks_uid + "\",\"provider\":\"" + aks_provider + "\",\"key_value\":\"" + aks_key + "\",\"updated_at\":\"now()\"}"
|
|
let _aks_resp: String = supabase_insert(aks_url, aks_service, "user_api_keys?on_conflict=user_id,provider", aks_row)
|
|
let aks_klen: Int = str_len(aks_key)
|
|
let aks_masked: String = if aks_klen <= 10 {
|
|
str_repeat("•", aks_klen)
|
|
} else {
|
|
str_slice(aks_key, 0, 6) + "••••" + str_slice(aks_key, aks_klen - 4, aks_klen)
|
|
}
|
|
return "{\"ok\":true,\"masked\":\"" + aks_masked + "\"}"
|
|
}
|
|
|
|
// ── API key provisioning — POST /api/api-keys/delete ─────────────────────
|
|
// Soft-deletes a provider key by clearing key_value for the authenticated user.
|
|
// Body: { access_token: "<jwt>", provider: "openai"|"anthropic"|"gemini"|"grok" }
|
|
if str_eq(path, "/api/api-keys/delete") {
|
|
let akd_jwt: String = json_get_string(body, "access_token")
|
|
let akd_provider: String = json_get_string(body, "provider")
|
|
if str_eq(akd_jwt, "") {
|
|
return "{\"__status__\":401,\"error\":\"missing_jwt\"}"
|
|
}
|
|
let akd_url: String = state_get("__supabase_project_url__")
|
|
let akd_anon: String = state_get("__supabase_anon_key__")
|
|
let akd_service: String = state_get("__supabase_service_key__")
|
|
if str_eq(akd_url, "") {
|
|
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
|
|
}
|
|
let akd_user: String = supabase_auth_user(akd_url, akd_anon, akd_jwt)
|
|
let akd_uid: String = json_get(akd_user, "id")
|
|
if str_eq(akd_uid, "") {
|
|
return "{\"__status__\":401,\"error\":\"invalid_jwt\"}"
|
|
}
|
|
let akd_row: String = "{\"user_id\":\"" + akd_uid + "\",\"provider\":\"" + akd_provider + "\",\"key_value\":\"\",\"updated_at\":\"now()\"}"
|
|
let _akd_resp: String = supabase_insert(akd_url, akd_service, "user_api_keys?on_conflict=user_id,provider", akd_row)
|
|
return "{\"ok\":true}"
|
|
}
|
|
|
|
// ── Fallback ──────────────────────────────────────────────────────────────
|
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
|
}
|
|
|
|
// ── Security header wrapper ───────────────────────────────────────────────────
|
|
//
|
|
// Injects mandatory security headers on every response. Called by
|
|
// handle_request which is the actual http_set_handler target; the inner
|
|
// dispatcher (handle_request_inner) returns plain bodies so all the existing
|
|
// route code is unchanged.
|
|
//
|
|
// Headers applied:
|
|
// Strict-Transport-Security — forces HTTPS for 2 years + preload
|
|
// X-Content-Type-Options — no MIME sniffing
|
|
// X-Frame-Options — no framing except same origin
|
|
// Referrer-Policy — full URL within origin, origin-only cross-site
|
|
// Permissions-Policy — deny geo/mic/camera
|
|
// Content-Security-Policy — allow self + trusted CDNs used by the app
|
|
|
|
fn sec_headers_json() -> String {
|
|
"{\"Strict-Transport-Security\":\"max-age=63072000; includeSubDomains; preload\","
|
|
+ "\"X-Content-Type-Options\":\"nosniff\","
|
|
+ "\"X-Frame-Options\":\"SAMEORIGIN\","
|
|
+ "\"Referrer-Policy\":\"strict-origin-when-cross-origin\","
|
|
+ "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\","
|
|
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}"
|
|
}
|
|
|
|
// Headers for compiled JS assets. Explicitly sets Content-Type so the browser
|
|
// treats them as JavaScript regardless of what http_detect_content_type()
|
|
// infers from the content (minified/obfuscated JS can trip the JSON heuristic).
|
|
// Cache-Control bumped to 1 year + immutable: JS bundles are content-addressed
|
|
// (hash in filename) so safe for Cloudflare to cache indefinitely at the edge.
|
|
fn js_headers_json() -> String {
|
|
"{\"Content-Type\":\"application/javascript; charset=utf-8\","
|
|
+ "\"Cache-Control\":\"public, max-age=31536000, immutable\","
|
|
+ "\"Strict-Transport-Security\":\"max-age=63072000; includeSubDomains; preload\","
|
|
+ "\"X-Content-Type-Options\":\"nosniff\","
|
|
+ "\"X-Frame-Options\":\"SAMEORIGIN\","
|
|
+ "\"Referrer-Policy\":\"strict-origin-when-cross-origin\","
|
|
+ "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\","
|
|
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}"
|
|
}
|
|
|
|
// Headers for static assets under /assets/ and /brand/.
|
|
// max-age=31536000 (1 year) + immutable tells Cloudflare to cache at the edge
|
|
// and never revalidate — assets are versioned by filename or content so stale
|
|
// delivery is not a risk. This eliminates Cloud Run hits for every image/font/svg.
|
|
// Security headers are included so asset responses are equally hardened even
|
|
// when served directly (e.g. Cloudflare bypass or direct origin fetch).
|
|
fn static_asset_headers_json() -> String {
|
|
"{\"Cache-Control\":\"public, max-age=31536000, immutable\","
|
|
+ "\"Strict-Transport-Security\":\"max-age=63072000; includeSubDomains; preload\","
|
|
+ "\"X-Content-Type-Options\":\"nosniff\","
|
|
+ "\"X-Frame-Options\":\"SAMEORIGIN\","
|
|
+ "\"Referrer-Policy\":\"strict-origin-when-cross-origin\","
|
|
+ "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\","
|
|
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}"
|
|
}
|
|
|
|
fn handle_request(method: String, path: String, headers: Map, body: String) -> String {
|
|
let inner_resp: String = handle_request_inner(method, path, headers, body)
|
|
// Detect envelope already set by inner handler (starts with
|
|
// {"el_http_response":1). If so, let it pass through unmodified —
|
|
// the status code it carries takes precedence and we must not
|
|
// double-wrap. (Currently inner never returns an envelope, but guard
|
|
// defensively so a future route returning http_response(...) still works.)
|
|
if str_starts_with(inner_resp, "{\"el_http_response\":1") {
|
|
return inner_resp
|
|
}
|
|
// Detect the __status__ convention used by many routes so we can forward
|
|
// the correct HTTP status code while still injecting security headers.
|
|
let status_code: Int = 200
|
|
if str_starts_with(inner_resp, "{\"__status__\":") {
|
|
let status_str: String = json_get(inner_resp, "__status__")
|
|
if !str_eq(status_str, "") {
|
|
let status_code = str_to_int(status_str)
|
|
}
|
|
}
|
|
http_response(status_code, sec_headers_json(), inner_resp)
|
|
}
|
|
|
|
// ── Startup ───────────────────────────────────────────────────────────────────
|
|
//
|
|
// Order matters:
|
|
// 1. Load Stripe config + origin from env (needed before founding count)
|
|
// 2. Seed founding count from file or Stripe API
|
|
// 3. Generate all HTML with the real count
|
|
// 4. Register with El HTTP runtime and serve
|
|
|
|
let landing_root: String = env("LANDING_ROOT")
|
|
let src_dir: String = if str_eq(landing_root, "") { cwd() + "/src" } else { landing_root }
|
|
let html_path: String = src_dir + "/index.html"
|
|
|
|
// Soul URL — where the demo soul is reachable.
|
|
// Defaults to localhost for local dev; set SOUL_URL for deployed environments.
|
|
let soul_url_env: String = env("SOUL_URL")
|
|
let soul_url: String = if str_eq(soul_url_env, "") { "http://localhost:7772" } else { soul_url_env }
|
|
state_set("__soul_url__", soul_url)
|
|
|
|
// Stripe config from environment — loaded first so founding count can use it.
|
|
let stripe_key: String = env("STRIPE_SECRET_KEY")
|
|
let stripe_pub_key: String = env("STRIPE_PUBLISHABLE_KEY")
|
|
let stripe_price_founding: String = env("STRIPE_PRICE_FOUNDING")
|
|
let stripe_price_professional: String = env("STRIPE_PRICE_PROFESSIONAL")
|
|
let family_child_price: String = env("STRIPE_PRICE_FAMILY_CHILD")
|
|
let stripe_webhook_secret: String = env("STRIPE_WEBHOOK_SECRET")
|
|
let license_api_url: String = env("NEURON_LICENSE_API_URL")
|
|
let resend_api_key: String = env("RESEND_API_KEY")
|
|
let supabase_anon_key: String = env("SUPABASE_ANON_KEY")
|
|
let supabase_service_key: String = env("SUPABASE_SERVICE_KEY")
|
|
let supabase_project_url: String = "https://ocojsghaonltunidkzpw.supabase.co"
|
|
let turnstile_secret_key: String = env("TURNSTILE_SECRET_KEY")
|
|
|
|
// Origin — drives Stripe redirect URLs; never hardcoded to localhost.
|
|
let neuron_origin_env: String = env("NEURON_ORIGIN")
|
|
let neuron_origin: String = if str_eq(neuron_origin_env, "") { "https://neurontechnologies.ai" } else { neuron_origin_env }
|
|
|
|
// Founding count — seeded from persist file or live Stripe query.
|
|
let sold_file: String = src_dir + "/founding_sold.txt"
|
|
let real_sold: Int = load_founding_count(sold_file, stripe_key)
|
|
|
|
// Ensure shares directory exists.
|
|
fs_mkdir(src_dir + "/shares")
|
|
|
|
// Generate all page HTML using the real founding count.
|
|
let page_html: String = page(real_sold, FOUNDING_TOTAL)
|
|
fs_write(html_path, page_html)
|
|
|
|
// Generate about page HTML.
|
|
let about_html_path: String = src_dir + "/about.html"
|
|
let about_html: String = page_open_seo(
|
|
"About Will Anderson — Neuron",
|
|
"Neuron was built by one person. Will Anderson — engineer, founder, and the sole author of every line of Neuron's code. This is his story.",
|
|
"/about",
|
|
"Neuron was built by one person. Will Anderson spent nearly two years building the AI that remembers you — the memory architecture, the inference infrastructure, everything from the ground up.",
|
|
"false"
|
|
) + about_page() + page_close()
|
|
fs_write(about_html_path, about_html)
|
|
|
|
// Generate terms pages HTML.
|
|
let terms_html_path: String = src_dir + "/terms.html"
|
|
let ent_terms_html_path: String = src_dir + "/enterprise-terms.html"
|
|
fs_write(terms_html_path, terms_page())
|
|
fs_write(ent_terms_html_path, enterprise_terms_page())
|
|
|
|
// Register with El HTTP runtime.
|
|
state_set("__html_file__", html_path)
|
|
state_set("__about_html_file__", about_html_path)
|
|
state_set("__terms_html_file__", terms_html_path)
|
|
state_set("__enterprise_terms_html_file__", ent_terms_html_path)
|
|
state_set("__src_dir__", src_dir)
|
|
state_set("__stripe_secret_key__", stripe_key)
|
|
state_set("__stripe_publishable_key__", stripe_pub_key)
|
|
state_set("__stripe_price_founding__", stripe_price_founding)
|
|
state_set("__stripe_price_professional__", stripe_price_professional)
|
|
state_set("__stripe_price_family_child__", family_child_price)
|
|
state_set("__license_api_url__", license_api_url)
|
|
state_set("__resend_api_key__", resend_api_key)
|
|
state_set("__supabase_anon_key__", supabase_anon_key)
|
|
state_set("__supabase_service_key__", supabase_service_key)
|
|
state_set("__supabase_project_url__", supabase_project_url)
|
|
state_set("__origin__", neuron_origin)
|
|
state_set("__founding_sold_file__", sold_file)
|
|
state_set("__founding_sold__", int_to_str(real_sold))
|
|
state_set("__founding_total__", int_to_str(FOUNDING_TOTAL))
|
|
state_set("__turnstile_secret_key__", turnstile_secret_key)
|
|
state_set("__stripe_webhook_secret__", stripe_webhook_secret)
|
|
persist_founding_count(real_sold)
|
|
|
|
println(color_bold("Neuron") + " - " + neuron_origin)
|
|
println(" HTML generated by El → " + html_path)
|
|
println(" About generated by El → " + about_html_path)
|
|
println(" Terms generated by El → " + terms_html_path)
|
|
println(" Ent. Terms generated by El → " + ent_terms_html_path)
|
|
println(" Assets → " + src_dir + "/assets/")
|
|
println("")
|
|
println(" Routes:")
|
|
println(" GET / → El-generated landing page")
|
|
println(" GET /about → El-generated about page")
|
|
println(" GET /legal/terms → Consumer terms of service")
|
|
println(" GET /legal/enterprise-terms → Enterprise agreement")
|
|
println(" GET /api/health → health check")
|
|
println(" GET /api/founding-count → founding counter JSON")
|
|
println(" POST /api/checkout → Stripe checkout session")
|
|
println(" POST /api/webhooks/stripe → Stripe webhook")
|
|
println(" GET /marketplace/success → post-purchase success page")
|
|
println(" GET /assets/* → static files")
|
|
println(" GET /js/* → compiled client-side JS (dist/js/)")
|
|
println(" GET /brand/* → brand files")
|
|
println(" GET /api/supabase-config → public Supabase config (URL + anon key)")
|
|
println("")
|
|
|
|
let port: Int = if str_eq(env("PORT"), "") { 3001 } else { str_to_int(env("PORT")) }
|
|
http_set_handler_v2("handle_request")
|
|
http_serve_v2(port, "handle_request")
|