diff --git a/src/main.el b/src/main.el
index 8ca4a48..c584df2 100644
--- a/src/main.el
+++ b/src/main.el
@@ -502,7 +502,7 @@ fn config_get(key: String) -> String {
// function - it serves __html_file__ directly with text/html.
// This handler covers /api/* and /brand/* routes.
-fn handle_request(method: String, path: String, body: String) -> String {
+fn handle_request_inner(method: String, path: String, body: String) -> String {
let src_dir: String = state_get("__src_dir__")
// ── Root — serve El-generated landing page ────────────────────────────────
@@ -520,7 +520,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
// ── robots.txt ────────────────────────────────────────────────────────────
if str_eq(path, "/robots.txt") {
- return "User-agent: *\nAllow: /\nSitemap: https://neurontechnologies.ai/sitemap.xml\n"
+ return "User-agent: *\nAllow: /\n"
}
// ── About page ────────────────────────────────────────────────────────────
@@ -990,12 +990,18 @@ fn handle_request(method: String, path: String, body: String) -> String {
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
+ // 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 gcs_bucket: String = env("GCS_SHARE_BUCKET")
- if !str_eq(gcs_bucket, "") {
- let attest_key: String = "attestations/" + attest_ts + "-" + attest_email + ".json"
- let gcs_ok: String = gcs_write(gcs_bucket, attest_key, record)
+ 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
@@ -1117,15 +1123,21 @@ fn handle_request(method: String, path: String, body: String) -> String {
}
state_set(rl_key, int_to_str(rl_count + 1) + "|" + int_to_str(today_day))
}
- // Turnstile: verify on first message only (tokens are single-use).
- // Per-message verification breaks chat flow. Forms get full verification.
+ // 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")
- if !str_eq(cf_token, "") {
- let ts_secret: String = "0x4AAAAAADHAZTok46L-l2sa9biSGpgN3GY"
+ 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/v0/siteverify", verify_body)
+ 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, "false") {
+ if !str_eq(is_valid, "true") {
return "{\"error\":\"Bot check failed. Please try again.\"}"
}
}
@@ -1617,9 +1629,15 @@ fn handle_request(method: String, path: String, body: String) -> String {
} 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
}
@@ -1827,6 +1845,52 @@ fn handle_request(method: String, path: String, body: String) -> String {
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' 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, body: String) -> String {
+ let inner_resp: String = handle_request_inner(method, path, 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:
@@ -1856,6 +1920,7 @@ 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")
@@ -1903,6 +1968,7 @@ 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)
persist_founding_count(real_sold)
println(color_bold("Neuron") + " - " + neuron_origin)
diff --git a/src/styles.el b/src/styles.el
index 2ee5d42..937d738 100644
--- a/src/styles.el
+++ b/src/styles.el
@@ -1828,7 +1828,7 @@ fn page_open() -> String {
button[disabled] { opacity: 0.6; cursor: not-allowed; }
-
+