Compare commits

..

11 Commits

Author SHA1 Message Date
will.anderson 00e62bb010 Fix free tier checkout and Stripe duplicate customers
Free tier:
- checkout-stripe.el bails out immediately for plan=free (no Stripe init)
- checkout-auth.el skips payment section reveal and initStripe for free plan
- checkout-free.el shows #free-success panel after auth (no card ever shown)
- /api/payment-intent returns early for free plan — no Stripe call

Stripe dedup (all paid plans):
- Stripe init now deferred to window.initStripe(email, name), called by
  checkout-auth.el after sign-in — email is known before intent is created
- /api/payment-intent finds-or-creates Stripe Customer by email before
  creating the PaymentIntent/SetupIntent and attaches customer upfront
- Eliminates the window between intent creation and /api/link-customer
  that was producing duplicate guest customers
2026-05-07 01:00:51 -05:00
will.anderson f0a6b55a13 fix: add -DHAVE_CURL to el_runtime.c compilation, restore el_runtime.o for soul-demo
Root cause: the staged el_runtime.c (from el.git) wraps the entire OTLP
observability section (emit_metric, emit_log, trace_span_start/end) in
#ifdef HAVE_CURL. Without -DHAVE_CURL, those symbols are compiled out,
causing the undefined reference linker errors.

libcurl IS available (installed via libcurl4-openssl-dev), so -DHAVE_CURL
correctly enables the OTLP code path.

Also reverts the soul-demo compile to use el_runtime.o (the pre-compiled
cached object) now that the object will contain the correct symbols.
2026-05-06 21:35:16 -05:00
will.anderson 843b6e07a7 Merge pull request 'fix: soul-demo emit_metric linker error — rebuild from source, compile with el_runtime.c' (#7) from fix/soul-demo-emit-metric into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 2m26s
2026-05-07 02:30:46 +00:00
will.anderson 0202b09d37 fix: rebuild soul-demo.c from source, compile against el_runtime.c directly
soul-demo.c was previously an older compiled artifact that triggered an
undefined reference to emit_metric/emit_log/trace_span_* at link time in CI.

Two fixes:
1. Rebuild soul-demo.c from soul-demo.el using current elc — cleaner
   codegen (no double-wrapped el_from_float), same logic, unix_timestamp
   collision with el_runtime.c removed.
2. Dockerfile.stage: compile soul-demo against el_runtime.c directly
   (not el_runtime.o) so all runtime symbols are always resolved from the
   staged source, bypassing any Docker layer cache divergence on el_runtime.o.
2026-05-06 21:30:25 -05:00
will.anderson f19403ba68 Merge pull request 'fix: security hardening from pentest findings' (#6) from fix/pentest-security-hardening into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Failing after 2m37s
2026-05-07 02:02:50 +00:00
will.anderson 8d741fac20 Fix pentest security findings
- Turnstile server-side verification: reject requests with no cf_token;
  read secret from TURNSTILE_SECRET_KEY env (no longer hardcoded); fix
  siteverify URL from v0 to v1
- Security headers: wrap all responses via http_response() with HSTS,
  X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
  Permissions-Policy, and Content-Security-Policy
- GCS error info leak: guard /share/<id> response — only return content
  that starts with '<' (valid HTML); GCS error JSON is silently 404d
- robots.txt: remove Sitemap reference to sitemap.xml that returns 404
- SRI hash: add integrity + crossorigin attributes to marked.min.js CDN tag
- Attestations bucket: write /api/attest records to GCS_ATTEST_BUCKET
  (dedicated private bucket) instead of the share bucket; falls back to
  GCS_SHARE_BUCKET if GCS_ATTEST_BUCKET is not set (legacy deploys)
2026-05-06 20:58:29 -05:00
will.anderson 28c47c11c9 ci: fix EL_HOME to use lang/ subdirectory for El repo clone
El repo is organized under lang/ — runtime and dist/platform binaries
are at lang/el-compiler/runtime/ and lang/dist/platform/, not at root.
Setting EL_HOME=$DEST/lang makes RUNTIME_SRC resolve correctly so
build-stage.sh can cp el_runtime.{c,h,js} from the right location.
2026-05-05 11:01:47 +00:00
Will Anderson 8a8762ad4f ci: trigger stage CI after API merge 2026-05-05 04:46:30 -05:00
Will Anderson a936d2ebb7 ci: trigger stage build after API merge 2026-05-05 04:45:07 -05:00
will.anderson c49a838aad Merge pull request 'promote: dev → stage' (#2) from dev into stage
promote: dev to stage
2026-05-05 09:40:51 +00:00
will.anderson 6075f49e8a Merge pull request 'feat(account): email/password sign-up on account page' (#2) from dev into stage
Stage — Build, push & deploy to marketing-stage / deploy-stage (push) Successful in 2m47s
2026-05-04 14:05:04 +00:00
12 changed files with 281 additions and 1270 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ jobs:
"https://will:${CHECKOUT_TOKEN}@git.neuralplatform.ai/neuron-technologies/el.git" \
"$DEST"
ls -la "$DEST" | head -5
echo "EL_HOME=$DEST" >> "$GITHUB_ENV"
echo "EL_HOME=$DEST/lang" >> "$GITHUB_ENV"
- name: Authenticate to GCP
id: auth
-35
View File
@@ -17,18 +17,6 @@ on:
- '.gitea/workflows/stage.yaml'
- '.gitea/workflows/deploy.yaml'
pull_request:
branches: [dev]
paths:
- 'src/**'
- 'dist/**'
- 'runtime/**'
- 'Dockerfile.stage'
- 'build-stage.sh'
- '.gitea/workflows/dev.yaml'
- '.gitea/workflows/stage.yaml'
- '.gitea/workflows/deploy.yaml'
workflow_dispatch:
jobs:
@@ -47,8 +35,6 @@ jobs:
fetch-depth: 2
- name: Clone el (provides elc compiler)
# push/workflow_dispatch only — pull_request events don't get secrets injected
if: github.event_name != 'pull_request'
env:
CHECKOUT_TOKEN: ${{ secrets.CHECKOUT_TOKEN }}
run: |
@@ -60,41 +46,20 @@ jobs:
"$DEST"
echo "EL_HOME=$DEST" >> "$GITHUB_ENV"
- name: Set up El SDK from committed runtime (PR builds)
# pull_request events have no secrets — build from committed bin/ and runtime/
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
DEST="${{ github.workspace }}/../foundation-el"
mkdir -p "$DEST/dist/platform" "$DEST/el-compiler/runtime"
cp bin/elc-linux-amd64 "$DEST/dist/platform/elc"
cp bin/elc-linux-amd64 "$DEST/dist/platform/elc-linux-amd64"
chmod +x "$DEST/dist/platform/elc" "$DEST/dist/platform/elc-linux-amd64"
cp runtime/el_runtime.c "$DEST/el-compiler/runtime/"
cp runtime/el_runtime.h "$DEST/el-compiler/runtime/"
cp runtime/el_runtime.js "$DEST/el-compiler/runtime/"
echo "EL_HOME=$DEST" >> "$GITHUB_ENV"
echo "El SDK set up from committed runtime files (no CHECKOUT_TOKEN needed)"
- name: Authenticate to GCP
if: github.event_name != 'pull_request'
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Set up gcloud SDK
if: github.event_name != 'pull_request'
uses: google-github-actions/setup-gcloud@v2
with:
project_id: neuron-785695
- name: Configure docker auth for Artifact Registry
if: github.event_name != 'pull_request'
run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
- name: Get elc (pre-built linux/amd64 from El repo)
# Only needed for push/workflow_dispatch — PR builds set up elc from committed bin/
if: github.event_name != 'pull_request'
run: |
set -euo pipefail
ELC_SRC="$EL_HOME/dist/platform/elc-linux-amd64"
+4 -15
View File
@@ -2,6 +2,7 @@ name: Stage — Build, push & deploy to marketing-stage
# Pipeline: build → push → deploy marketing-stage → smoke test.
# STOPS HERE. No prod deploy. Merge to main when stage looks good.
# Triggered: 2026-05-05 (promote fix/gallery-layout-account-otp)
on:
push:
@@ -14,16 +15,6 @@ on:
- 'build-stage.sh'
- '.gitea/workflows/stage.yaml'
pull_request:
branches: [stage]
paths:
- 'src/**'
- 'dist/**'
- 'runtime/**'
- 'Dockerfile.stage'
- 'build-stage.sh'
- '.gitea/workflows/stage.yaml'
workflow_dispatch:
inputs:
tag:
@@ -88,7 +79,7 @@ jobs:
git clone --depth 1 \
"https://will:${CHECKOUT_TOKEN}@git.neuralplatform.ai/neuron-technologies/el.git" \
"$DEST"
echo "EL_HOME=$DEST" >> "$GITHUB_ENV"
echo "EL_HOME=$DEST/lang" >> "$GITHUB_ENV"
- name: Authenticate to GCP
uses: google-github-actions/auth@v2
@@ -144,13 +135,13 @@ jobs:
docker tag "marketing:${{ steps.tag.outputs.tag }}" "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest"
- name: Push image
if: steps.changetype.outputs.asset_only != 'true' && github.event_name != 'pull_request'
if: steps.changetype.outputs.asset_only != 'true'
run: |
docker push "${{ steps.tag.outputs.image }}"
docker push "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest"
- name: Asset-only fast build
if: steps.changetype.outputs.asset_only == 'true' && github.event_name != 'pull_request'
if: steps.changetype.outputs.asset_only == 'true'
env:
IMAGE: ${{ steps.tag.outputs.image }}
run: |
@@ -175,7 +166,6 @@ jobs:
echo "Fast asset build complete"
- name: Deploy to marketing-stage
if: github.event_name != 'pull_request'
id: deploy-stage
env:
IMAGE: ${{ steps.tag.outputs.image }}
@@ -204,7 +194,6 @@ jobs:
--quiet
- name: Smoke test stage
if: github.event_name != 'pull_request'
run: |
set -euo pipefail
STAGE_URL="${{ steps.deploy-stage.outputs.stage_url }}"
+4 -1
View File
@@ -28,7 +28,10 @@ COPY runtime/el_runtime.c runtime/el_runtime.h ./
# Pre-compile el_runtime as a separate cached layer.
# el_runtime.c changes rarely; main.c changes every run.
# Splitting this out means el_runtime.o is cached across builds when only main.c changes.
RUN cc -O2 -c el_runtime.c -I. -o el_runtime.o
# -DHAVE_CURL: the staged el_runtime.c (from el.git) guards the OTLP observability
# section (emit_metric, emit_log, trace_span_*) behind #ifdef HAVE_CURL.
# libcurl IS installed above, so define HAVE_CURL to enable those functions.
RUN cc -O2 -DHAVE_CURL -c el_runtime.c -I. -o el_runtime.o
# ── Build neuron-web ──────────────────────────────────────────────────────────
#
+121 -122
View File
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -97,7 +97,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
<div id=\"auth-section\" " + (if is_free { "" } else { "style=\"display:none;\"" }) + ">
" + (if is_free { "
<p class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\">Create your account.</p>
<p class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\">No charge today. Add your card to reserve your spot - you won&#39;t be billed until you upgrade.</p>
<p class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\">No card required. Your account is free, forever.</p>
" } else { "
<p class=\"label\" style=\"margin-bottom: 1.25rem;\">Sign in (optional)</p>
<p class=\"checkout-auth-hint\">Sign in to link this purchase to an existing account. Or skip and create one later - we'll match it to your email.</p>
@@ -135,6 +135,16 @@ fn checkout_page(plan: String, pub_key: String) -> String {
</div>
</div>
<!-- Free-tier success panel: shown after account creation, no card needed -->
" + (if is_free { "
<div id=\"free-success\" style=\"display:none; text-align:center; padding: 2.5rem 1rem;\">
<div style=\"font-size:2.5rem; margin-bottom:1.25rem;\">&#10003;</div>
<p class=\"label\" style=\"margin-bottom:.75rem; color:var(--navy);\">You&#39;re in.</p>
<p class=\"checkout-auth-hint\" style=\"margin-bottom:2rem;\">Your free account is ready. Download Neuron to get started.</p>
<a href=\"/marketplace\" class=\"checkout-submit\" style=\"display:inline-block; text-decoration:none; padding:.875rem 2rem;\">Go to your account &#8594;</a>
</div>
" } else { "" }) + "
<!-- Payment form (visible immediately - no auth wall) -->
<div id=\"payment-section\" " + (if is_free { "style=\"display:none;\"" } else { "" }) + ">
<div id=\"auth-badge\" style=\"display:none; margin-bottom: 1.5rem;\"></div>
+10 -5
View File
@@ -33,8 +33,11 @@ fn main() -> Void {
if (user && user.id) { window._neuronSupaId = user.id; }
var auth = document.getElementById('auth-section');
if (auth) auth.style.display = 'none';
var payment = document.getElementById('payment-section');
if (payment) payment.style.display = '';
var isFree = (window.NEURON_CFG || {}).plan === 'free';
if (!isFree) {
var payment = document.getElementById('payment-section');
if (payment) payment.style.display = '';
}
if (user) {
var badge = document.getElementById('auth-badge');
@@ -55,9 +58,11 @@ fn main() -> Void {
if (emailEl) emailEl.value = user.email;
}
var userEmail = user ? (user.email || '') : '';
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
if (typeof initStripe === 'function') initStripe(userEmail, userName);
if (!isFree) {
var userEmail = user ? (user.email || '') : '';
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
if (typeof window.initStripe === 'function') window.initStripe(userEmail, userName);
}
}
function checkExistingSession() {
+8 -5
View File
@@ -1,15 +1,18 @@
// checkout-free.el -- Free plan: reveal payment section after auth completes.
// Watches the auth-badge element; when it becomes visible, shows payment-section.
// checkout-free.el -- Free plan: show success panel after auth completes.
// Watches the auth-badge element; when it becomes visible, hides the auth
// section and shows the free-success panel. No card required for free tier.
// Compiled with: elc --target=js --bundle --minify --obfuscate
fn main() -> Void {
native_js("(function() {
var pay = document.getElementById('payment-section');
if (!pay) return;
var success = document.getElementById('free-success');
var auth = document.getElementById('auth-section');
if (!success) return;
var timer = setInterval(function() {
var badge = document.getElementById('auth-badge');
if (badge && badge.offsetParent !== null) {
pay.style.display = '';
if (auth) auth.style.display = 'none';
success.style.display = '';
clearInterval(timer);
}
}, 150);
+17 -6
View File
@@ -31,8 +31,13 @@ fn main() -> Void {
if (spinner) spinner.style.display = loading ? '' : 'none';
}
// Free plan has no payment form bail out entirely.
if (str_eq(PLAN, 'free')) return;
window._neuronMode = 'payment';
var paymentEl = null;
var userEmail = '';
var userName = '';
function appearance() {
return {
@@ -80,7 +85,7 @@ fn main() -> Void {
return fetch('/api/payment-intent', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ plan: PLAN, timing: timing })
body: JSON.stringify({ plan: PLAN, timing: timing, email: userEmail, name: userName })
})
.then(function(r) { return r.json(); })
.then(function(data) {
@@ -117,11 +122,17 @@ fn main() -> Void {
});
}
fetchAndMount();
var tNow = document.getElementById('timing-now');
var tLater = document.getElementById('timing-later');
if (tNow) tNow.addEventListener('change', fetchAndMount);
if (tLater) tLater.addEventListener('change', fetchAndMount);
// Don't init Stripe at page load wait for auth.
// checkout-auth.el calls window.initStripe(email, name) after sign-in.
window.initStripe = function(email, name) {
userEmail = email || '';
userName = name || '';
fetchAndMount();
var tNow = document.getElementById('timing-now');
var tLater = document.getElementById('timing-later');
if (tNow) tNow.addEventListener('change', fetchAndMount);
if (tLater) tLater.addEventListener('change', fetchAndMount);
};
var form = document.getElementById('payment-form');
if (form) form.addEventListener('submit', async function(e) {
+104 -29
View File
@@ -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
@@ -584,23 +584,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
}
let timing: String = json_get_string(body, "timing")
if str_eq(timing, "") { let timing = "now" }
// Free tier: SetupIntent save card details without charging.
// Card is stored on a Stripe Customer; billing begins only if the
// user later upgrades to a paid plan.
// Free tier: no card required. Return immediately no Stripe interaction.
if str_eq(plan, "free") {
let si_body: String = "automatic_payment_methods[enabled]=true"
+ "&usage=off_session"
+ "&metadata[plan]=free"
let auth_header: String = "Bearer " + stripe_key
let si_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/setup_intents",
si_body,
auth_header)
if str_starts_with(si_resp, "{") {
let inner: String = str_slice(si_resp, 1, str_len(si_resp))
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
}
return si_resp
return "{\"plan\":\"free\",\"free\":true,\"no_payment_required\":true}"
}
// Hard cap: block founding checkouts when 1,000 spots are filled
if str_eq(plan, "founding") {
@@ -612,6 +598,27 @@ fn handle_request(method: String, path: String, body: String) -> String {
}
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")
}
}
// 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") {
@@ -620,6 +627,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
+ "&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,
@@ -642,6 +650,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
+ "&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,
@@ -990,12 +999,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 +1132,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 +1638,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 +1854,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 +1929,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 +1977,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)
+1 -1
View File
@@ -1828,7 +1828,7 @@ fn page_open() -> String {
button[disabled] { opacity: 0.6; cursor: not-allowed; }
</style>
<script src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\"></script>
<script src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\" integrity=\"sha384-948ahk4ZmxYVYOc+rxN1H2gM1EJ2Duhp7uHtZ4WSLkV4Vtx5MUqnV+l7u9B+jFv+\" crossorigin=\"anonymous\"></script>
<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>
<noscript><style>.reveal { opacity: 1 !important; transform: none !important; }</style></noscript>