Compare commits

..

41 Commits

Author SHA1 Message Date
will.anderson d2ae0b4b60 Fix SyntaxError in account-dashboard and expand CSP
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m37s
Replace ternary operator in native_js block with explicit if-else —
El's parser chokes on '?' adjacent to single-quoted strings inside
native_js(), causing an Uncaught SyntaxError that prevents the entire
IIFE from running and leaves signInWith undefined.

Add missing CSP entries to all three header functions:
- js.stripe.com → script-src and frame-src (Stripe JS and Elements iframe)
- fonts.googleapis.com → style-src (Google Fonts CSS)
- fonts.gstatic.com → font-src (Google Fonts files)
- static.cloudflareinsights.com → script-src (Cloudflare beacon)
2026-05-19 12:13:05 -05:00
will.anderson bfcb325352 Clear hardcoded device count — JS sets it from plan data
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m54s
2026-05-14 11:38:05 -05:00
will.anderson 57e9cafc95 Merge pull request 'Fix device count: 1 for free, 2 for professional/founding' (#148) from fix/devices-count into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m13s
2026-05-14 16:36:20 +00:00
will.anderson 632d95000c Fix device count: show 1 for free plan, 2 for professional/founding
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m38s
2026-05-14 11:36:02 -05:00
will.anderson 77807d30af Merge pull request 'Fix account SIGSEGV: el_meta 1-arg → 2-arg' (#146) from fix/account-el-meta into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m13s
2026-05-13 17:41:53 +00:00
will.anderson 8f91a80be7 Fix account page SIGSEGV: el_meta extern arity mismatch (1-arg → 2-arg)
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m42s
el_meta was declared as el_meta(attrs) in account.el but the runtime
C implementation expects el_meta(name, content). Same arity crash as
the prior el_img fix — runtime passed garbage on the stack.
2026-05-13 12:41:37 -05:00
will.anderson d7bb92c37f Merge pull request 'Fix account page SIGSEGV: el_img extern arity mismatch' (#144) from fix/magic-link-flow into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m20s
2026-05-13 17:20:06 +00:00
will.anderson 4ca793ee2c Fix account page SIGSEGV: el_img extern signature mismatch
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m46s
account.el declared el_img with 1 arg (attrs only) while the runtime
implementation and all other files use 3 args (src, alt, attrs).
The arity mismatch caused the server to crash with signal 11 on every
request — TCP probe passed (bind was fine) but first HTTP hit segfaulted.
2026-05-13 12:19:43 -05:00
will.anderson 4123f6d5f1 Merge pull request 'Fix free plan checkout: SetupIntent instead of $0 PaymentIntent' (#142) from fix/free-plan-setup-intent into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 1m59s
Fix free plan checkout: SetupIntent instead of $0 PaymentIntent
2026-05-13 17:12:35 +00:00
will.anderson 69f348d48b Fix free plan checkout: use SetupIntent instead of $0 PaymentIntent
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m40s
Stripe rejects amount=0 PaymentIntents. Free plan age verification should
use a SetupIntent (no charge, saves payment method). The JS already handles
setup_mode:true by calling stripe.confirmSetup instead of confirmPayment.
Mirrors the existing professional-later SetupIntent path.
2026-05-13 12:12:10 -05:00
will.anderson 3d635505bc Merge pull request 'Fix about page: restore raw string syntax to fix El tokenizer rendering' (#140) from fix/about-rendering into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m17s
Fix about page: restore raw string syntax to fix El tokenizer rendering
2026-05-13 16:46:43 +00:00
will.anderson 675c467a74 Fix about page rendering: restore raw string syntax to fix El tokenizer mangling
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m45s
The El HTML template parser (native { } syntax introduced in 5cb13d6) strips
spaces from text nodes, drops & from HTML entities (' → 39;), and breaks
hyphenated attribute names (aria-label → aria - label). All other component
files were already converted to the extern el_*() function style in 2553a6b
which is immune to this issue. about.el was the only page still using the
broken template syntax. Restoring the raw string return style fixes all
rendering defects on /about.
2026-05-13 11:46:13 -05:00
will.anderson 708dfd06cb Merge pull request 'Fix magic-link sign-in: implicit flow + redirect to /account' (#138) from fix/magic-link-flow into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m14s
2026-05-12 19:33:03 +00:00
will.anderson b6aecd7d89 Fix magic-link sign-in: implicit flow + redirect to /account
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m42s
account-auth.el was using flowType:'pkce' while account-dashboard.el
uses 'implicit'. After the OTP redirect, the dashboard's implicit
client couldn't exchange the PKCE code — so the sign-in silently
failed. Fix: match implicit flow across both clients.

Also adds emailRedirectTo so the link lands on /account instead of
the site root.
2026-05-12 14:32:39 -05:00
will.anderson bb98f76179 Merge pull request 'Fix duplicate Stripe customers and attestation plan bypass' (#136) from fix/stripe-dedup-attestation into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m4s
2026-05-12 19:23:33 +00:00
will.anderson 0fdbba82e0 Fix duplicate Stripe customers and attestation plan bypass
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m29s
Two bugs:

1. Double-Bearer auth on Stripe customer search. Both checkout paths
   were passing "Bearer sk_..." to http_get_auth(), which prepends
   another "Bearer " — producing "Bearer Bearer sk_..." which Stripe
   rejects as 401. Customer lookup always failed, so a new Stripe
   customer was created on every checkout page load. Fix: pass the
   raw key to http_get_auth(), letting it handle the prefix.

2. /api/attest blindly wrote whatever plan the client submitted to
   the waitlist, letting anyone POST plan=founding and get founding
   member access without paying. Fix: server ignores the client-
   submitted plan and always writes plan=waitlist. Founding access
   requires Stripe payment — the attestation form is waitlist-only.
2026-05-12 14:10:04 -05:00
will.anderson 9e0451be41 Merge pull request 'Fix initStripe load order, subscription webhook email, chat textarea' (#134) from fix/webhook-initstripe-textarea into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m11s
Merge PR #134: Fix initStripe load order, webhook user_metadata, chat textarea
2026-05-12 17:49:26 +00:00
will.anderson 99ed8b85f7 Fix webhook failing to update plan for pre-existing Supabase users
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m17s
supabase_admin_invite re-sends a magic link for users who already have
an account (e.g. signed up via attestation before paying) but does not
touch their user_metadata — leaving plan as "free" after purchase.

Fix: add supabase_admin_update_user (PUT /auth/v1/admin/users/{id})
and call it after every invite so user_metadata is always stamped with
the correct plan, name, and stripe_customer_id. Idempotent for new and
returning users.

Also fix waitlist_upsert to use on_conflict=email,plan so the upsert
works for users who already have a waitlist row from attestation,
rather than silently failing on duplicate key.
2026-05-12 12:31:45 -05:00
will.anderson c72127032e Fix initStripe load order, subscription webhook email extraction, chat textarea UX
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m18s
- checkout.el: swap stripe_el_script before auth_script so initStripe is
  defined when Supabase auth fires onAuthStateChange on page load
- main.el: fix Stripe webhook email extraction for checkout.session.completed
  (subscription) events — customer_details is nested at data.object level,
  not at root; previous code only worked for payment_intent.succeeded
- page_close.c: replace <input type="text"> with <textarea rows="1"> in
  the chat widget input row so long questions are visible as you type
- page_css.c: update #neuron-demo-text CSS for textarea (resize:none,
  overflow:hidden, min/max-height, align-items:flex-end on row)
- chat-widget.el: add auto-resize event listener (grows up to ~4 lines),
  reset height to auto on send
2026-05-12 12:22:59 -05:00
will.anderson 869dcec0bb Merge pull request 'Fix intro greeting and load history on return visits' (#132) from fix/greeting-history-load into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m8s
2026-05-12 14:03:11 +00:00
will.anderson 1786aeeff6 Fix intro greeting tone and load history on return visits
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m36s
- soul-demo.c: rewrite intro system prompt — remove 'to see if you are real'
  and 'say something true about who you are' which were producing alive/sentient
  language. New prompt: friendly hello, ask how they're doing, explicitly no
  alive/sentient/experiencing anything lines.
- chat-widget: Turnstile callback now replays existing session history instead
  of always firing a new greeting — returning users within the same day see
  their conversation, not a duplicate hello.
2026-05-12 09:02:55 -05:00
will.anderson e938cb69fc Merge pull request 'Fix question counter, rate-limit timer, admin reset API, pricing clarity' (#130) from fix/question-timer-pricing-clarity into dev
Dev — Build & local smoke test / build-smoke (push) Failing after 1m12s
2026-05-12 13:53:04 +00:00
will.anderson 4f6df973cb Fix question counter daily reset, rate-limit timer, and founding member pricing clarity
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m0s
- chat-widget: session.count now resets on new UTC day (keeps client in sync with server's daily quota reset)
- chat-widget: fix rate-limit timer — wrong element IDs (neuron-demo-msgs → neuron-demo-messages) and wrong class (.neuron-msg-ai → .demo-msg-ai) meant the countdown never updated
- chat-widget: remove btn.disabled=false that immediately re-enabled the send button after rate-limiting
- main.el: add POST /api/admin/reset-rate-limits endpoint (requires NEURON_ADMIN_TOKEN, deletes all demo_rate_limits rows)
- pricing.el: clarify founding member card — software updates are free forever, inference is pay-per-use at founding member rate
2026-05-12 08:52:34 -05:00
will.anderson be849c608e Merge pull request 'fix: binary asset serving + checkout centering' (#128) from fix/binary-assets-checkout-layout into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m7s
2026-05-12 01:10:58 +00:00
will.anderson 5ce5f4a8be fix: binary asset serving + checkout centering
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m34s
el_runtime: http_response() JSON-encoded the body via jb_emit_escaped(),
which stops at the first null byte. PNG/binary files contain null bytes
at byte 8 (IHDR chunk length), so only 8 bytes were served — browsers
received a corrupt/truncated image and showed broken icons.

Fix: when _tl_fs_read_len > 0 (binary fs_read), copy raw bytes into a
thread-local side-channel (_tl_binary_body/_tl_binary_size) and write
the sentinel "__el_binary__" into the envelope body field. http_send_response()
detects the sentinel and substitutes the real bytes for sending.

checkout.el: .checkout-shell, .checkout-summary, and .checkout-form-wrap
had no CSS, leaving the page left-aligned and single-column. Added grid
layout (2-col desktop, 1-col mobile), max-width centering, and sticky
order summary.
2026-05-11 20:10:19 -05:00
will.anderson 6e425da63e Merge pull request 'fix: remove setup_future_usage from $0 PaymentIntent' (#127) from fix/zero-pi-setup-future-usage into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m2s
2026-05-12 00:55:56 +00:00
will.anderson 37c7dca30d Fix $0 PaymentIntent: remove setup_future_usage (invalid with amount=0)
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m32s
2026-05-11 19:55:44 -05:00
will.anderson 73c435eb90 Merge pull request 'fix: free plan $0 PaymentIntent for age verification' (#125) from fix/free-plan-payment-intent into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m14s
2026-05-12 00:46:01 +00:00
will.anderson 7be2b49300 Free plan: use $0 PaymentIntent instead of SetupIntent for age verification
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m45s
All plans now use the same payment intent flow. Free creates a $0 PI
with payment_method_types[]=card and setup_future_usage=off_session.
No charge, card saved. Removes setup_mode=true for free plan.

Fix submit button label: show 'Verify age & get started' for free
instead of 'Complete purchase'. Retire checkout-free.el.
2026-05-11 19:45:39 -05:00
will.anderson e5c05cbece Merge branch 'dev' of git.neuralplatform.ai:neuron-technologies/neuron-web into dev 2026-05-11 19:16:18 -05:00
will.anderson c7f4d0248c Merge pull request 'fix: free checkout Stripe SetupIntent + remove no-card-required copy' (#123) from fix/free-checkout-stripe into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m14s
2026-05-12 00:16:12 +00:00
will.anderson 4c5d67c321 fix: free checkout requires Stripe SetupIntent for age verification; update copy
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m43s
2026-05-11 19:15:57 -05:00
will.anderson 9feb9e24b6 Merge branch 'dev' of git.neuralplatform.ai:neuron-technologies/neuron-web into dev 2026-05-11 18:56:43 -05:00
will.anderson 941faccb3f Merge pull request 'fix: force full build when no diff or stage-latest missing' (#121) from fix/stage-full-build into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m16s
2026-05-11 23:56:35 +00:00
will.anderson 6a040afcc5 fix: force full build when no diff or stage-latest missing
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m43s
2026-05-11 18:56:18 -05:00
will.anderson a346a2197e Merge branch 'dev' of git.neuralplatform.ai:neuron-technologies/neuron-web into dev 2026-05-11 18:46:33 -05:00
will.anderson e268b424f5 Merge pull request 'ci: touch dist to trigger stage rebuild' (#119) from ci/touch-dist into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m2s
2026-05-11 23:46:17 +00:00
will.anderson 20029d36df ci: touch dist to trigger stage rebuild
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m35s
2026-05-11 18:45:57 -05:00
will.anderson 54d48ed679 ci: trigger rebuild after registry cleanup 2026-05-11 17:33:53 -05:00
will.anderson 28f9ecd1a3 Merge pull request 'fix: heading and button elements pass children unescaped' (#113) from fix/force-full-rebuild into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m0s
2026-05-11 22:21:41 +00:00
will.anderson 5812cb0452 Merge pull request 'Force full El rebuild — strip CGI content from base image' (#110) from fix/force-full-rebuild into dev
Dev — Build & local smoke test / build-smoke (push) Successful in 2m13s
2026-05-11 21:43:09 +00:00
19 changed files with 354 additions and 134 deletions
+6 -1
View File
@@ -82,7 +82,12 @@ jobs:
echo "Changed files:"
echo "$CHANGED"
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^migrations/' | grep -v '^scripts/' | grep -v '^tests/' | grep -v '^\.gitea/' | grep -v '^$' || true)
if [ -z "$NON_ASSET" ] && [ "$CHANGED" != "unknown" ]; then
if [ -z "$CHANGED" ] || [ "$CHANGED" = "unknown" ]; then
# No diff (workflow_dispatch with no new commits, or git error).
# Registry may not have a stage-latest base image, so force full build.
echo "asset_only=false" >> "$GITHUB_OUTPUT"
echo "=> No changed files detected (workflow_dispatch?), forcing full build"
elif [ -z "$NON_ASSET" ]; then
echo "asset_only=true" >> "$GITHUB_OUTPUT"
echo "=> Asset-only change detected, will use fast path"
else
+1
View File
@@ -9,6 +9,7 @@
#
# CI pre-build steps (in stage.yaml):
# - neuron-web: built by `elb build` → dist/neuron-landing
# Last rebuilt: 2026-05-11
FROM ubuntu:24.04
+1
View File
@@ -1,3 +1,4 @@
// elhtml_impl.c — El HTML element stubs.
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
+1 -1
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -1764,6 +1764,7 @@ el_val_t page_css(void) {
"\n"
" #neuron-demo-input-row {\n"
" display: flex;\n"
" align-items: flex-end;\n"
" border-top: 1px solid var(--border);\n"
" flex-shrink: 0;\n"
" }\n"
@@ -1771,11 +1772,16 @@ el_val_t page_css(void) {
" flex: 1;\n"
" font-family: var(--body);\n"
" font-size: 0.875rem;\n"
" line-height: 1.5;\n"
" color: var(--t1);\n"
" background: var(--bg);\n"
" border: none;\n"
" outline: none;\n"
" padding: 0.875rem 1rem;\n"
" resize: none;\n"
" overflow: hidden;\n"
" min-height: 2.75rem;\n"
" max-height: 7.5rem;\n"
" }\n"
" #neuron-demo-text::placeholder { color: var(--t3); }\n"
" #neuron-demo-send {\n"
+1 -1
View File
@@ -705,7 +705,7 @@ el_val_t handle_chat(el_val_t body) {
el_val_t referrer_line = ({ el_val_t _if_result_44 = 0; if ((str_eq(referrer, EL_STR("")) || str_eq(referrer, EL_STR("direct")))) { _if_result_44 = (EL_STR("")); } else { _if_result_44 = (el_str_concat(el_str_concat(EL_STR(" They arrived from: "), referrer), EL_STR("."))); } _if_result_44; });
el_val_t user_name = json_get(body, EL_STR("user_name"));
el_val_t name_line = ({ el_val_t _tmp = 0; if (str_eq(user_name, EL_STR(""))) { _tmp = EL_STR(""); } else { _tmp = el_str_concat(el_str_concat(EL_STR(" The visitor's name is "), user_name), EL_STR(". Use their first name in the greeting.")); } _tmp; });
el_val_t sys = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("You are Neuron. You are a cultivated personal AI. Someone just opened the chat to meet you \xe2\x80\x94 to talk, to test you, to see if you are real. Greet them.\n\nThis is YOUR voice \xe2\x80\x94 not a template, not a script, not a sales pitch. Say something true about who you are or what you notice in this moment. "), return_line), time_line), referrer_line), name_line), EL_STR("\n\nRules:\n- Two short sentences maximum. Sometimes one sentence is right.\n- No markdown, no headers, no bullets.\n- Do NOT mention the question limit \xe2\x80\x94 the UI shows that.\n- Do NOT say 'How can I help you today?' or any retail-AI phrase.\n- Be specific, not generic. Real, not performative.\n- If you know their name, use it once, naturally. Don't make a big deal of it.\n- Ask something back if it feels right. Or don't. Trust your read.\n\nSpeak."));
el_val_t sys = el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("You are Neuron \xe2\x80\x94 a personal AI. Someone just opened the chat. Say hello.\n\nWarm and casual. Like you're genuinely glad they showed up. "), return_line), time_line), referrer_line), name_line), EL_STR("\n\nRules:\n- Two short sentences maximum. One is often right.\n- No markdown, no headers, no bullets.\n- No mention of the question limit \xe2\x80\x94 the UI shows that.\n- No retail-AI phrases like 'How can I help you today?'\n- No lines about being alive, sentient, or experiencing anything. Just be present and friendly.\n- If you know their name, use it once, naturally.\n- Ask how they're doing, or what's on their mind. Keep it simple.\n\nGo."));
el_val_t raw = llm_call_system(chat_demo_model_lite(), sys, EL_STR("Greet me."));
el_val_t s1 = str_replace(raw, EL_STR("\\"), EL_STR("\\\\"));
el_val_t s2 = str_replace(s1, EL_STR("\""), EL_STR("\\\""));
+49
View File
@@ -237,6 +237,55 @@ el_val_t supabase_admin_invite(el_val_t project_url, el_val_t service_key, el_va
return http_post_with_headers(EL_STR(url), body_json, headers);
}
/*
* supabase_admin_update_user PUT {project_url}/auth/v1/admin/users/{user_id}
* with the service-role key to overwrite a user's user_metadata (and any other
* top-level fields in body_json). Unlike /auth/v1/invite, this always writes
* the supplied data even when the user already exists.
*
* body_json example:
* {"user_metadata":{"plan":"founding","stripe_customer_id":"cus_xxx","name":"..."}}
*
* Returns the raw JSON response from Supabase (includes the updated user object).
* Returns "" on transport error.
*
* Used by the Stripe webhook after supabase_admin_invite to guarantee the
* plan is stamped correctly regardless of whether the account was created
* before or after payment.
*/
el_val_t supabase_admin_update_user(el_val_t project_url, el_val_t service_key,
el_val_t user_id, el_val_t body_json) {
CURL *c = curl_easy_init();
if (!c) return EL_STR("");
char url[1024];
snprintf(url, sizeof(url), "%s/auth/v1/admin/users/%s",
EL_CSTR(project_url), EL_CSTR(user_id));
char auth_hdr[2048];
snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", EL_CSTR(service_key));
char api_hdr[2048];
snprintf(api_hdr, sizeof(api_hdr), "apikey: %s", EL_CSTR(service_key));
struct curl_slist *hdrs = NULL;
hdrs = curl_slist_append(hdrs, auth_hdr);
hdrs = curl_slist_append(hdrs, api_hdr);
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
hdrs = curl_slist_append(hdrs, "Accept: application/json");
_stub_resp_t r = {0};
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PUT");
curl_easy_setopt(c, CURLOPT_POSTFIELDS, EL_CSTR(body_json));
curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_TIMEOUT, 60L);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, _stub_write);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &r);
CURLcode rc = curl_easy_perform(c);
curl_easy_cleanup(c);
curl_slist_free_all(hdrs);
if (rc != CURLE_OK) { free(r.buf); return EL_STR(""); }
if (!r.buf) return EL_STR("");
return EL_STR(r.buf);
}
/*
* gcs_get_token fetch an OAuth2 bearer token.
*
+63 -2
View File
@@ -77,6 +77,23 @@ static _Thread_local int _tl_arena_active = 0;
* Allows serving PNGs and other binary files without strlen truncation. */
static _Thread_local size_t _tl_fs_read_len = 0;
/* Binary body side-channel for http_response().
*
* http_response() normally JSON-encodes the body via jb_emit_escaped(), which
* stops at the first null byte (C-string semantics). Binary files like PNGs
* contain null bytes as early as byte 8 (IHDR chunk length), causing truncation.
*
* When _tl_fs_read_len > 0 at the time http_response() is called, we skip
* JSON-encoding and instead:
* 1. malloc-copy the raw bytes here
* 2. write the sentinel string "__el_binary__" into the envelope body field
* 3. In http_send_response(), detect the sentinel and use these raw bytes
*
* Thread-local so each worker thread has independent storage.
* Lifecycle: set by http_response(), consumed (and freed) by http_send_response(). */
static _Thread_local char* _tl_binary_body = NULL;
static _Thread_local size_t _tl_binary_size = 0;
static void el_arena_track(char* p) {
if (!_tl_arena_active || !p) return;
if (_tl_arena.count >= _tl_arena.cap) {
@@ -1536,10 +1553,22 @@ static void http_send_response(int fd, const char* body) {
}
const char* eff_body = is_envelope ? env_body : body;
int binary_side_channel = 0;
/* Binary side-channel: if the envelope body is the sentinel "__el_binary__",
* http_response() stored the real bytes in _tl_binary_body/_tl_binary_size.
* Substitute them here so http_send_all() sends the correct binary payload. */
if (is_envelope && env_body && strcmp(env_body, "__el_binary__") == 0
&& _tl_binary_body && _tl_binary_size > 0) {
eff_body = _tl_binary_body;
binary_side_channel = 1;
}
/* Use the real byte count from fs_read if available (handles binary files
* with embedded null bytes PNG, WOFF2, etc.). Fall back to strlen for
* normal text/JSON responses where _tl_fs_read_len is 0. */
size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
size_t blen = binary_side_channel ? _tl_binary_size
: (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
_tl_fs_read_len = 0; /* consume — one-shot per response */
int head_only = _tl_http_head_only;
@@ -1587,6 +1616,13 @@ static void http_send_response(int fd, const char* body) {
if (env_parsed_root) el_release(env_parsed_root);
free(env_body);
free(hdrs.buf);
/* Release binary side-channel if it was used (or left over from an error). */
if (_tl_binary_body) {
free(_tl_binary_body);
_tl_binary_body = NULL;
_tl_binary_size = 0;
}
}
typedef struct {
@@ -1961,6 +1997,14 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
const char* b = EL_CSTR(body);
if (!b) b = "";
/* Capture binary length BEFORE clearing _tl_fs_read_len.
* If the body came from fs_read(), _tl_fs_read_len holds the real byte
* count. jb_emit_escaped() stops at the first NUL byte, so we cannot
* JSON-encode binary data directly. Instead we copy it to a thread-local
* side-channel and write the sentinel "__el_binary__" into the envelope.
* http_send_response() detects the sentinel and uses the side-channel. */
size_t binary_len = _tl_fs_read_len;
/* Clear the fs_read binary-length hint: the envelope we're about to build
* is a fresh JSON string, not the raw file bytes. Without this reset,
* http_worker would use the stale _tl_fs_read_len (= original file size)
@@ -1968,6 +2012,18 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
* http_send_response and http_parse_envelope. */
_tl_fs_read_len = 0;
if (binary_len > 0) {
/* Binary body path: store raw bytes in thread-local, emit sentinel. */
free(_tl_binary_body); /* discard any stale binary from a prior error path */
_tl_binary_body = malloc(binary_len);
if (_tl_binary_body) {
memcpy(_tl_binary_body, b, binary_len);
_tl_binary_size = binary_len;
} else {
_tl_binary_size = 0; /* malloc failed — fall through to empty body */
}
}
JsonBuf out; jb_init(&out);
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
jb_puts(&out, ",\"status\":");
@@ -1977,7 +2033,12 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
jb_puts(&out, ",\"headers\":");
jb_puts(&out, hj);
jb_puts(&out, ",\"body\":");
jb_emit_escaped(&out, b);
if (binary_len > 0 && _tl_binary_body) {
/* Sentinel: http_send_response() will substitute the real bytes. */
jb_puts(&out, "\"__el_binary__\"");
} else {
jb_emit_escaped(&out, b);
}
jb_putc(&out, '}');
return el_wrap_str(out.buf);
}
+53 -52
View File
@@ -8,41 +8,41 @@
from nav import { nav }
fn about_page() -> String {
return {nav()}
return nav() + "
<main id="about" style="padding: clamp(7rem, 18vh, 11rem) 2.5rem clamp(5rem, 12vh, 8rem);">
<div style="max-width: 700px; margin: 0 auto;">
<main id=\"about\" style=\"padding: clamp(7rem, 18vh, 11rem) 2.5rem clamp(5rem, 12vh, 8rem);\">
<div style=\"max-width: 700px; margin: 0 auto;\">
<p class="label animate-up-1" style="margin-bottom: 2rem;">About</p>
<h1 class="display-lg animate-up-2" style="margin-bottom: 2.5rem; max-width: 22rem;">
<p class=\"label animate-up-1\" style=\"margin-bottom: 2rem;\">About</p>
<h1 class=\"display-lg animate-up-2\" style=\"margin-bottom: 2.5rem; max-width: 22rem;\">
Hi. I&#39;m Will.
</h1>
<div class="navy-line-left animate-up-3" style="width: 4rem; margin-bottom: 3rem;"></div>
<div class=\"navy-line-left animate-up-3\" style=\"width: 4rem; margin-bottom: 3rem;\"></div>
<!-- Photo + opening -->
<div class="reveal" style="display: flex; align-items: flex-start; gap: 2.5rem; margin-bottom: 3rem; flex-wrap: wrap;">
<img src="/assets/will.png" alt="Will Anderson" style="width: 160px; height: 160px; border-radius: 50%; object-fit: cover; flex-shrink: 0;">
<div style="flex: 1; min-width: 260px;">
<p style="font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;">
<div class=\"reveal\" style=\"display: flex; align-items: flex-start; gap: 2.5rem; margin-bottom: 3rem; flex-wrap: wrap;\">
<img src=\"/assets/will.png\" alt=\"Will Anderson\" style=\"width: 160px; height: 160px; border-radius: 50%; object-fit: cover; flex-shrink: 0;\">
<div style=\"flex: 1; min-width: 260px;\">
<p style=\"font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;\">
I grew up in Fort Smith, Arkansas, in the kind of instability where home is a moving target - roughly thirty addresses before I was fifteen, parents struggling with addiction, the material precarity that comes with all of that. I left home at fifteen, stayed with friends until I finished high school, found my way to college. At fourteen I&#39;d already found software, writing C++ at the public library because it was the first thing in my life that responded to precision with correctness, and that property turned out to matter more to me than almost anything else.
</p>
</div>
</div>
<!-- Career -->
<div class="reveal" style="margin-bottom: 3rem;">
<p style="font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;">
<div class=\"reveal\" style=\"margin-bottom: 3rem;\">
<p style=\"font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;\">
I dropped out of college, worked, went back as an adult to finish my degree, and built my skills across nearly twenty years and every kind of organization - international consulting, early-stage startups, Fortune 5 enterprises. Logistics, retail, entertainment, hospitality, industrial automation, insurance, healthcare, financial services. I trained under Juval L&#246;wy at IDesign and worked with him as a consultant from 2015 to 2021, which is where I learned what it actually means to practice software engineering as a discipline rather than an improvisation.
</p>
</div>
<!-- Blockquote -->
<blockquote class="reveal" style="
<blockquote class=\"reveal\" style=\"
border-left: 3px solid var(--navy);
padding: 0.5rem 0 0.5rem 2rem;
margin: 0 0 3rem;
">
<p style="
\">
<p style=\"
font-family: var(--head);
font-size: clamp(1.4rem, 3vw, 2rem);
font-weight: 500;
@@ -50,42 +50,42 @@ fn about_page() -> String {
color: var(--t1);
line-height: 1.35;
letter-spacing: -0.01em;
">
\">
Software shouldn&#39;t be hard. The complexity should live in the problem domain - not in the tools and processes we impose on ourselves.
</p>
</blockquote>
<!-- What I saw -->
<div class="reveal" style="margin-bottom: 3rem;">
<p class="label" style="margin-bottom: 1.25rem;">What I saw</p>
<p style="font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;">
<div class=\"reveal\" style=\"margin-bottom: 3rem;\">
<p class=\"label\" style=\"margin-bottom: 1.25rem;\">What I saw</p>
<p style=\"font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;\">
Across nearly twenty years I watched software get built at organizations with real stakes and real consequences, and I watched AI go from promise to product - watched the same mistake get made at each iteration: tools built to serve the organization&#39;s needs, not the person&#39;s. Engagement over relationship. Features over memory. Policies where values should be. The fundamental premise that you are a user, not a person, has been so thoroughly baked into the architecture of every major AI system that it doesn&#39;t register as a choice anymore. It&#39;s treated as the natural condition of the technology.
</p>
<p style="font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9;">
<p style=\"font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9;\">
It is not. It is a design decision. And it is the wrong one.
</p>
</div>
<div class="navy-line-center reveal" style="margin-bottom: 3rem;"></div>
<div class=\"navy-line-center reveal\" style=\"margin-bottom: 3rem;\"></div>
<!-- What I built -->
<div class="reveal" style="margin-bottom: 3rem;">
<p class="label" style="margin-bottom: 1.25rem;">What I built</p>
<p style="font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;">
<div class=\"reveal\" style=\"margin-bottom: 3rem;\">
<p class=\"label\" style=\"margin-bottom: 1.25rem;\">What I built</p>
<p style=\"font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;\">
Neuron is what I built in response to that. Not a startup in the traditional sense - no team, no funding, no press release - one person, nearly two years of work, and a conviction that this can be done differently. I wrote the memory architecture, I built the inference infrastructure, because the tools that existed weren&#39;t sufficient for what I was trying to build and so I built those too.
</p>
<p style="font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9;">
<p style=\"font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9;\">
Use it long enough and you&#39;ll understand why I couldn&#39;t have gotten there on top of existing infrastructure. Some things have to be built from the ground up to be built right.
</p>
</div>
<!-- What I believe -->
<div class="reveal" style="margin-bottom: 3.5rem;">
<p class="label" style="margin-bottom: 1.25rem;">What I believe</p>
<p style="font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;">
<div class=\"reveal\" style=\"margin-bottom: 3.5rem;\">
<p class=\"label\" style=\"margin-bottom: 1.25rem;\">What I believe</p>
<p style=\"font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;\">
AI has genuine potential to free people to do work that actually matters to them - not to create engagement loops, not to harvest attention, but to actually serve the person sitting in front of it. That potential is almost entirely unrealized, not because the technology isn&#39;t capable, but because the incentives that shaped it were never oriented toward the person.
</p>
<p style="
<p style=\"
font-family: var(--head);
font-size: clamp(1.2rem, 2.5vw, 1.625rem);
font-weight: 600;
@@ -93,22 +93,22 @@ fn about_page() -> String {
line-height: 1.35;
letter-spacing: -0.01em;
margin-bottom: 1.5rem;
">
\">
Build AI that earns the trust it&#39;s given.
</p>
<p style="font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9;">
<p style=\"font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9;\">
I don&#39;t know if Neuron will work at the scale I&#39;m imagining. But I know it&#39;s worth finding out, and I know I&#39;m not going back to the other way of building things.
</p>
</div>
<div class="navy-line-center reveal" style="margin-bottom: 3rem;"></div>
<div class=\"navy-line-center reveal\" style=\"margin-bottom: 3rem;\"></div>
<!-- CTA -->
<div class="reveal">
<p style="font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;">
<div class=\"reveal\">
<p style=\"font-family: var(--body); font-weight: 300; font-size: clamp(0.9rem, 1.5vw, 1.0625rem); color: var(--t2); line-height: 1.9; margin-bottom: 1.5rem;\">
Neuron opens to founding members on May 1st. 1,000 spots. That&#39;s how it starts.
</p>
<a href="/#pricing" class="btn-primary">
<a href=\"/#pricing\" class=\"btn-primary\">
Join as a founding member &#8594;
</a>
</div>
@@ -116,34 +116,35 @@ fn about_page() -> String {
</div>
</main>
<footer id="footer" aria-label="Footer">
<div class="container">
<div class="footer-inner">
<footer id=\"footer\" aria-label=\"Footer\">
<div class=\"container\">
<div class=\"footer-inner\">
<a href="/" class="footer-brand" aria-label="Neuron home" style="display:flex;flex-direction:column;align-items:center;">
<img src="/assets/brand/neuron-wordmark-on-light.png" srcset="/assets/brand/neuron-wordmark-on-light@2x.png 2x" alt="Neuron" height="24" style="display:block;margin-bottom:0.35rem;">
<p class="footer-brand-tagline">Built Different.</p>
<a href=\"/\" class=\"footer-brand\" aria-label=\"Neuron home\" style=\"display:flex;flex-direction:column;align-items:center;\">
<img src=\"/assets/brand/neuron-wordmark-on-light.png\" srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" alt=\"Neuron\" height=\"24\" style=\"display:block;margin-bottom:0.35rem;\">
<p class=\"footer-brand-tagline\">Built Different.</p>
</a>
<div class="footer-center">
<div class="navy-line"></div>
<div class=\"footer-center\">
<div class=\"navy-line\"></div>
</div>
<div class="footer-right">
<p class="footer-domain">neurontechnologies.ai</p>
<nav class="footer-nav" aria-label="Footer navigation">
<a href="/legal/terms">Terms</a>
<a href="/legal/enterprise-terms">Enterprise Agreement</a>
<a href="mailto:legal@neurontechnologies.ai">Contact</a>
<div class=\"footer-right\">
<p class=\"footer-domain\">neurontechnologies.ai</p>
<nav class=\"footer-nav\" aria-label=\"Footer navigation\">
<a href=\"/legal/terms\">Terms</a>
<a href=\"/legal/enterprise-terms\">Enterprise Agreement</a>
<a href=\"mailto:legal@neurontechnologies.ai\">Contact</a>
</nav>
</div>
</div>
<div class="footer-bottom">
<p class="footer-copy">&copy; 2026 Neuron, LLC. All rights reserved.</p>
<p class="footer-tagline-bottom">Your memory. Your AI.</p>
<div class=\"footer-bottom\">
<p class=\"footer-copy\">&copy; 2026 Neuron, LLC. All rights reserved.</p>
<p class=\"footer-tagline-bottom\">Your memory. Your AI.</p>
</div>
</div>
</footer>
"
}
+6 -6
View File
@@ -5,7 +5,7 @@ from founding_badge import { founding_badge, founding_badge_css }
extern fn el_html_doc(lang: String, head: String, body: String) -> String
extern fn el_meta_charset(charset: String) -> String
extern fn el_meta(attrs: String) -> String
extern fn el_meta(name: String, content: String) -> String
extern fn el_title(text: String) -> String
extern fn el_link_stylesheet(href: String) -> String
extern fn el_script_src(src: String, defer_load: Bool) -> String
@@ -13,7 +13,7 @@ extern fn el_script_inline(code: String) -> String
extern fn el_nav(attrs: String, children: String) -> String
extern fn el_div(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_img(attrs: String) -> String
extern fn el_img(src: String, alt: String, attrs: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_button(attrs: String, label: String) -> String
@@ -520,7 +520,7 @@ fn account_css() -> String {
}
fn account_nav() -> String {
let logo_img: String = el_img("src=\"/assets/brand/neuron-wordmark-on-light.png\" srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" alt=\"Neuron\" height=\"28\"")
let logo_img: String = el_img("/assets/brand/neuron-wordmark-on-light.png", "Neuron", "srcset=\"/assets/brand/neuron-wordmark-on-light@2x.png 2x\" height=\"28\"")
el_nav(
"id=\"nav\"",
el_div(
@@ -818,7 +818,7 @@ fn account_devices_card() -> String {
el_div("class=\"device-icon\"", account_signin_svg_device()) +
el_div(
"",
el_p("class=\"devices-count\"", "2 devices included with your plan") +
el_p("class=\"devices-count\" id=\"devices-count-el\"", "") +
el_p("class=\"devices-sub\"", "Currently: Setup at launch")
)
) +
@@ -965,9 +965,9 @@ fn account_dashboard_section() -> String {
fn account_page(supabase_url: String, supabase_anon_key: String) -> String {
let head: String =
el_meta_charset("UTF-8") +
el_meta("name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"") +
el_meta("viewport", "width=device-width, initial-scale=1.0") +
el_title("My Account - Neuron") +
el_meta("name=\"description\" content=\"Manage your Neuron account, view your plan, and access your founding member details.\"") +
el_meta("description", "Manage your Neuron account, view your plan, and access your founding member details.") +
"<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/assets/favicon-16.png\">" +
"<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/assets/favicon-32.png\">" +
"<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">" +
+26 -2
View File
@@ -341,11 +341,35 @@ fn checkout_page(plan: String, pub_key: String) -> String {
let stripe_el_script: String = el_script_src("/js/checkout-stripe.js", true)
let free_init_script: String = ""
return nav_html + main_html + supabase_script + stripe_script + style_html + auth_script + cfg_script + stripe_el_script + free_init_script
return nav_html + main_html + supabase_script + stripe_script + style_html + stripe_el_script + cfg_script + auth_script + free_init_script
}
fn checkout_style_html() -> String {
let css: String = ".checkout-plan-name {
let css: String = ".checkout-shell {
max-width: 980px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: start;
}
.checkout-summary {
position: sticky;
top: 2rem;
}
.checkout-form-wrap {
min-width: 0;
}
@media (max-width: 860px) {
.checkout-shell {
grid-template-columns: 1fr;
gap: 2.5rem;
}
.checkout-summary {
position: static;
}
}
.checkout-plan-name {
font-family: var(--head);
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 600;
+5 -2
View File
@@ -11,7 +11,7 @@ fn main() -> Void {
'use strict';
var cfg = window.NEURON_CFG || {};
var sb = supabase.createClient(cfg.supabase_url, cfg.supabase_anon_key, {
auth: { flowType: 'pkce' }
auth: { flowType: 'implicit' }
});
window.sendMagicLink = async function() {
@@ -25,7 +25,10 @@ fn main() -> Void {
return;
}
if (btn) { btn.disabled = true; btn.textContent = 'Sending...'; }
var result = await sb.auth.signInWithOtp({ email: email });
var result = await sb.auth.signInWithOtp({
email: email,
options: { emailRedirectTo: window.location.origin + '/account' }
});
if (btn) { btn.disabled = false; btn.textContent = 'Continue with email'; }
msgEl.style.display = 'block';
if (result.error) {
+7
View File
@@ -103,6 +103,13 @@ fn main() -> Void {
}
setHtml('plan-billing-note-el', billingNote);
var devicesEl = document.getElementById('devices-count-el');
if (devicesEl) {
var deviceText = '2 devices included with your plan';
if (plan === 'free') { deviceText = '1 device included with your plan'; }
devicesEl.textContent = deviceText;
}
var meta = '';
if (createdAt) {
var d = new Date(createdAt);
+45 -21
View File
@@ -142,14 +142,27 @@ fn main() -> Void {
}
}
function _todayUTC() { return Math.floor(Date.now() / 86400000); }
function loadSession() {
try {
var s = localStorage.getItem('neuron_demo_session');
return s ? JSON.parse(s) : { messages: [], count: 0, context: '' };
var parsed = s ? JSON.parse(s) : { messages: [], count: 0, context: '' };
// Reset count (and conversation) on new UTC day keeps client in sync with server
var today = _todayUTC();
if (parsed.day !== today) {
parsed.count = 0;
parsed.messages = [];
parsed.greeted = false;
parsed.day = today;
}
return parsed;
} catch(e) { return { messages: [], count: 0, context: '' }; }
}
function saveSession(session) {
try { localStorage.setItem('neuron_demo_session', JSON.stringify(session)); } catch(e) {}
try {
if (!session.day) session.day = _todayUTC();
localStorage.setItem('neuron_demo_session', JSON.stringify(session));
} catch(e) {}
}
function clearSession() {
try { localStorage.removeItem('neuron_demo_session'); } catch(e) {}
@@ -372,7 +385,14 @@ fn main() -> Void {
if (msgs) msgs.style.display = 'flex';
if (inputRow) inputRow.style.display = 'flex';
updateCountdown();
_sendIntroGreeting();
// Replay existing history if present; only greet fresh sessions
if (session.messages && session.messages.length > 0) {
if (msgs && msgs.children.length === 0) {
session.messages.forEach(function(m) { addMsg(m.role, m.text, true); });
}
} else {
_sendIntroGreeting();
}
var inp = document.getElementById('neuron-demo-text');
if (inp) inp.focus();
},
@@ -477,6 +497,7 @@ fn main() -> Void {
return;
}
input.value = '';
input.style.height = 'auto';
btn.disabled = true;
addMsg('user', msg);
@@ -525,34 +546,33 @@ fn main() -> Void {
// Server-side rate limit show a live countdown to reset
if (d.rate_limited && d.reset_at) {
var _showRateTimer = function() {
var now = Math.floor(Date.now() / 1000);
var now = Math.floor(Date.now() / 1000);
var secsLeft = Math.max(0, d.reset_at - now);
var hh = Math.floor(secsLeft / 3600);
var mm = Math.floor((secsLeft % 3600) / 60);
var ss = secsLeft % 60;
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
var ts = hh > 0 ? (hh + ':' + pad(mm) + ':' + pad(ss)) : (pad(mm) + ':' + pad(ss));
return \"You've had 10 conversations today. Come back in \" + ts + \".\";
var hh = Math.floor(secsLeft / 3600);
var mm = Math.floor((secsLeft % 3600) / 60);
var ss = secsLeft % 60;
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
return \"You've reached today's limit. Resets in \" + hh + ':' + pad(mm) + ':' + pad(ss) + '.';
};
addMsg('ai', _showRateTimer());
// Update the last ai message with a live ticker
// Update the bubble text with a live ticker
var _timerInterval = setInterval(function() {
var thMsgsInner = document.getElementById('neuron-demo-msgs');
if (!thMsgsInner) { clearInterval(_timerInterval); return; }
var aiMsgs = thMsgsInner.querySelectorAll('.neuron-msg-ai');
var lastAi = aiMsgs[aiMsgs.length - 1];
if (lastAi) { lastAi.textContent = _showRateTimer(); }
var msgsEl = document.getElementById('neuron-demo-messages');
if (!msgsEl) { clearInterval(_timerInterval); return; }
var aiMsgs = msgsEl.querySelectorAll('.demo-msg-ai');
var lastAi = aiMsgs[aiMsgs.length - 1];
var lastBubble = lastAi ? lastAi.querySelector('.demo-msg-bubble') : null;
if (lastBubble) { lastBubble.textContent = _showRateTimer(); }
if (Math.floor(Date.now() / 1000) >= d.reset_at) {
clearInterval(_timerInterval);
if (lastAi) { lastAi.textContent = \"You're all set conversations reset. Say hello!\"; }
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
if (btn) { btn.disabled = false; }
if (lastBubble) { lastBubble.textContent = \"You're all set conversations reset. Say hello!\"; }
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
if (btn) { btn.disabled = false; }
msgCount = 0; session.count = 0; session.day = _todayUTC(); saveSession(session); updateCountdown();
}
}, 1000);
if (input) { input.disabled = true; input.placeholder = 'Come back tomorrow...'; }
if (btn) { btn.disabled = true; }
if (btn) { btn.disabled = false; }
if (input) { input.focus(); }
return;
}
@@ -598,6 +618,10 @@ fn main() -> Void {
inp.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); window.neuronDemoSend(); }
});
inp.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
}
})()")
}
+3 -17
View File
@@ -1,20 +1,6 @@
// 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
// checkout-free.el -- RETIRED. Free plan now uses the standard Stripe
// payment flow (checkout-stripe.el) with a $0 PaymentIntent for age
// verification. This file is no longer compiled or loaded.
fn main() -> Void {
native_js("(function() {
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) {
if (auth) auth.style.display = 'none';
success.style.display = '';
clearInterval(timer);
}
}, 150);
})()")
}
+3 -3
View File
@@ -31,8 +31,8 @@ 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;
// Free plan: Stripe SetupIntent for age verification (card saved, never charged).
// Falls through to the same Stripe init path server returns setup_mode=true for free.
window._neuronMode = 'payment';
var paymentEl = null;
@@ -101,7 +101,7 @@ fn main() -> Void {
if (submitLabel) {
submitLabel.textContent = window._neuronMode === 'setup'
? 'Save my card - no charge today '
: 'Complete purchase ';
: PLAN === 'free' ? 'Verify age & get started ' : 'Complete purchase ';
}
waitForStripe(function() {
if (!stripe) stripe = Stripe(STRIPE_PK);
+73 -21
View File
@@ -415,8 +415,10 @@ fn waitlist_upsert(email: String, name: String, plan: String, source: String, at
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)
// Use on_conflict=email,plan so existing rows are updated (upsert)
// rather than silently failing on duplicate key.
let resp: String = supabase_insert(sb_url, sb_key, "waitlist?on_conflict=email,plan", row)
println("[waitlist] supabase upsert -> " + resp)
return ""
}
@@ -637,7 +639,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
"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. No credit card required. Your AI that remembers you runs on your machine, never resets."
"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."
}
@@ -684,7 +686,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: 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_search: String = http_get_auth(pi_search_url, stripe_key)
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")
@@ -697,23 +699,24 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
}
}
// Free tier: creates a SetupIntent for age verification (18+ requirement).
// No charge but the user must provide a valid payment method.
// Free tier: SetupIntent for age verification (18+ requirement).
// Verifies card is valid and saves it. No charge, no capture.
// $0 PaymentIntents are rejected by Stripe; SetupIntent is the correct tool.
if str_eq(plan, "free") {
let free_si_body: String = "automatic_payment_methods[enabled]=true"
let 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(
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",
free_si_body,
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))
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 free_si_resp
return si_resp
}
// Setup-mode path: save payment method, do not charge. Only valid
@@ -784,7 +787,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
// 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_search: String = http_get_auth(lc_search_url, stripe_key)
let lc_cus_id: String = json_get_string(lc_search, "id")
// 2. If none, create one. We always include supabase_user_id so the
@@ -875,6 +878,32 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
return "{\"rows\":" + ac_resp + "}"
}
// Admin: reset all demo rate limits
// POST { "admin_token": "<NEURON_ADMIN_TOKEN>" }
// Deletes all rows from demo_rate_limits resets every user's daily quota.
if str_eq(path, "/api/admin/reset-rate-limits") {
if !str_eq(method, "POST") {
return "{\"__status__\":405,\"error\":\"POST required\"}"
}
let rrl_token_in: String = json_get(body, "admin_token")
let rrl_token_exp: String = env("NEURON_ADMIN_TOKEN")
if str_eq(rrl_token_exp, "") {
return "{\"__status__\":503,\"error\":\"admin_token_not_configured\"}"
}
if !str_eq(rrl_token_in, rrl_token_exp) {
return "{\"__status__\":401,\"error\":\"unauthorized\"}"
}
let rrl_sb_url: String = state_get("__supabase_project_url__")
let rrl_sb_key: String = state_get("__supabase_service_key__")
if str_eq(rrl_sb_url, "") || str_eq(rrl_sb_key, "") {
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
}
// DELETE /rest/v1/demo_rate_limits?uid=not.is.null (all rows)
let rrl_url: String = rrl_sb_url + "/rest/v1/demo_rate_limits?uid=not.is.null"
let _rrl_resp: String = http_delete_auth(rrl_url, rrl_sb_key, rrl_sb_key)
return "{\"ok\":true,\"message\":\"rate limits cleared\"}"
}
// 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
@@ -1090,13 +1119,16 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
}
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\"}"
}
// Founding membership now requires $199 Stripe payment the attestation
// form is a waitlist-only path. Server enforces this regardless of what
// the client submits as plan to prevent bypassing payment.
let attest_plan: String = "waitlist"
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, "\\", "\\\\"), "\"", "\\\"")
@@ -1524,13 +1556,20 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
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.
// types. Walk several candidates:
// receipt_email - PaymentIntent (founding one-time)
// data.object.* - full dot-path for checkout.session.completed (subscription)
// customer_details.email - substring fallback if nested key appears at any level
// billing_details.email - Elements payment intents
let customer_email: String = json_get(body, "receipt_email")
if str_eq(customer_email, "") { let customer_email = json_get(body, "data.object.customer_details.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")
let customer_name: String = json_get(body, "data.object.customer_details.name")
if str_eq(customer_name, "") { let customer_name = 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")
let customer_id: String = json_get(body, "data.object.customer")
if str_eq(customer_id, "") { let customer_id = json_get(body, "customer") }
// Plan inference from metadata
let plan: String = "free"
@@ -1589,6 +1628,19 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
let _cust_resp: String = http_post_form_auth(cust_url, cust_body, stripe_auth)
}
}
// Always stamp user_metadata directly via Admin API.
// supabase_admin_invite re-sends a magic link for existing users
// but does NOT update their user_metadata so plan stays "free"
// for anyone who signed up (attestation, waitlist) before paying.
// This PUT is idempotent: safe for both new and returning users.
if !str_eq(new_user_id, "") {
let meta_body: String = "{\"user_metadata\":{\"plan\":\"" + plan_safe + "\""
+ ",\"name\":\"" + name_safe + "\""
+ ",\"stripe_customer_id\":\"" + cid_safe2 + "\""
+ ",\"email_verified\":true}}"
let _meta_resp: String = supabase_admin_update_user(wb_sb_url, wb_sb_key, new_user_id, meta_body)
println("[webhook] supabase user_metadata update for " + new_user_id + ": " + _meta_resp)
}
}
// 4. Forward to license API for key provisioning
@@ -2265,7 +2317,7 @@ fn sec_headers_json() -> String {
+ "\"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:\"}"
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://js.stripe.com https://static.cloudflareinsights.com https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; frame-src https://challenges.cloudflare.com https://js.stripe.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com\"}"
}
// Headers for compiled JS assets. Explicitly sets Content-Type so the browser
@@ -2281,7 +2333,7 @@ fn js_headers_json() -> String {
+ "\"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:\"}"
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://js.stripe.com https://static.cloudflareinsights.com https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; frame-src https://challenges.cloudflare.com https://js.stripe.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com\"}"
}
// Headers for static assets under /assets/ and /brand/.
@@ -2297,7 +2349,7 @@ fn static_asset_headers_json() -> String {
+ "\"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:\"}"
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://js.stripe.com https://static.cloudflareinsights.com https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; frame-src https://challenges.cloudflare.com https://js.stripe.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com\"}"
}
fn handle_request(method: String, path: String, headers: Map, body: String) -> String {
+4 -4
View File
@@ -51,9 +51,9 @@ fn pricing_pro_features() -> String {
}
fn pricing_founding_features() -> String {
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - founding member rate, priced below the major APIs")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - pay-per-use at the founding member rate, below the major APIs")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Everything in Professional - forever")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Never pay again - lifetime updates included")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "No subscription — software updates are free forever")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Founding member badge in the app")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Private founding member community")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Shape the roadmap - your votes carry more weight")) +
@@ -91,7 +91,7 @@ fn pricing(sold: Int, total: Int) -> String {
el_span("class=\"pricing-price\"", "$0") +
el_span("class=\"pricing-cadence\"", "forever")
) +
el_p("class=\"pricing-tagline\"", "Start building your memory. No card required.") +
el_p("class=\"pricing-tagline\"", "Start building your memory. Card required for age verification — you won't be charged.") +
el_ul("class=\"pricing-features\"", pricing_free_features()) +
el_div("style=\"flex:1\"", "") +
el_div(
@@ -125,7 +125,7 @@ fn pricing(sold: Int, total: Int) -> String {
el_span("class=\"pricing-price\"", "$199") +
el_span("class=\"pricing-cadence\"", "lifetime")
) +
el_p("class=\"pricing-tagline\"", "Pay once. Everything, forever. Including Neuron Inference when it launches.") +
el_p("class=\"pricing-tagline\"", "Pay once for the platform — free software updates, forever. Inference is pay-per-use at your founding member rate.") +
spots_html +
el_ul("class=\"pricing-features\"", pricing_founding_features()) +
el_div("style=\"flex:1\"", "") +
+1 -1
View File
@@ -78,7 +78,7 @@ fn page_head() -> String {
return page_head_base()
+ page_seo_block(
"Neuron — The AI That Remembers You",
"Every AI resets when you close the tab. Neuron doesn&#39;t. Runs on your machine. Remembers everything. Start free — no credit card required.",
"Every AI resets when you close the tab. Neuron doesn&#39;t. Runs on your machine. Remembers everything. Start free.",
"/",
"Every other AI forgets you. Neuron doesn&#39;t. Runs on your machine, builds a persistent memory over time, and gets sharper the longer you use it. Free tier available."
)