Compare commits

..

16 Commits

Author SHA1 Message Date
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 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 b6bb25e79e fix: heading and button elements pass children unescaped
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m32s
el_h1/h2/h3/h4 and el_button were calling el_escape() on their
content, converting any HTML children (e.g. <span class="gold">)
into literal entity text on screen.

These functions accept composed HTML children, not raw text — they
should pass the argument through like el_div/el_p/el_span do.
el_text, el_attr, el_title, el_textarea, and el_img keep escaping
(they handle actual text/attribute values, not HTML children).
2026-05-11 17:21:19 -05:00
16 changed files with 162 additions and 240 deletions
+2 -2
View File
@@ -55,7 +55,7 @@ jobs:
echo "$CHANGED"
# Asset-only: files that don't require rebuilding the El binary.
# migrations/, scripts/, tests/ are data/infra/test changes — no binary rebuild needed.
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/privacy\.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)
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
echo "asset_only=true" >> "$GITHUB_OUTPUT"
echo "=> Asset-only change detected, will use fast path"
@@ -104,7 +104,7 @@ jobs:
- name: Touch HTML placeholder files
# El binary regenerates these at startup via fs_write. They must exist
# in the build context for Dockerfile COPY to succeed.
run: touch src/index.html src/about.html src/terms.html src/privacy.html src/enterprise-terms.html
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
# ── El SDK setup ──────────────────────────────────────────────────────
+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 -1
View File
@@ -96,7 +96,7 @@ RUN apt-get update \
&& groupadd -r landing && useradd -r -g landing landing
COPY --from=builder /build/landing /usr/local/bin/landing
COPY src/index.html src/about.html src/terms.html src/privacy.html src/enterprise-terms.html /srv/landing/
COPY src/index.html /srv/landing/index.html
COPY src/assets /srv/landing/assets
ENV LANDING_ROOT=/srv/landing
+3 -2
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
@@ -37,8 +38,8 @@ COPY src/llms.txt /srv/landing/llms.txt
# rewrite them with the freshly-rendered page (extracted JS asset paths,
# updated chat widget, etc.). Without this they stay as their COPY'd root-
# owned shells and the served HTML never reflects post-COPY source edits.
COPY src/about.html src/terms.html src/privacy.html src/enterprise-terms.html src/index.html /srv/landing/
RUN chown landing:landing /srv/landing/about.html /srv/landing/terms.html /srv/landing/privacy.html /srv/landing/enterprise-terms.html /srv/landing/index.html /srv/landing/llms.txt
COPY src/about.html src/terms.html src/enterprise-terms.html src/index.html /srv/landing/
RUN chown landing:landing /srv/landing/about.html /srv/landing/terms.html /srv/landing/enterprise-terms.html /srv/landing/index.html /srv/landing/llms.txt
COPY dist/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
+21 -20
View File
@@ -1,3 +1,4 @@
// elhtml_impl.c — El HTML element stubs.
#include <stdint.h>
#include <stdlib.h>
#include "el_runtime.h"
@@ -19,11 +20,11 @@ el_val_t el_li(el_val_t attrs, el_val_t children);
el_val_t el_p(el_val_t attrs, el_val_t children);
el_val_t el_span(el_val_t attrs, el_val_t children);
el_val_t el_form(el_val_t attrs, el_val_t children);
el_val_t el_h1(el_val_t attrs, el_val_t text);
el_val_t el_h2(el_val_t attrs, el_val_t text);
el_val_t el_h3(el_val_t attrs, el_val_t text);
el_val_t el_h4(el_val_t attrs, el_val_t text);
el_val_t el_button(el_val_t attrs, el_val_t label);
el_val_t el_h1(el_val_t attrs, el_val_t children);
el_val_t el_h2(el_val_t attrs, el_val_t children);
el_val_t el_h3(el_val_t attrs, el_val_t children);
el_val_t el_h4(el_val_t attrs, el_val_t children);
el_val_t el_button(el_val_t attrs, el_val_t children);
el_val_t el_a(el_val_t href, el_val_t attrs, el_val_t children);
el_val_t el_input(el_val_t type_attr, el_val_t attrs);
el_val_t el_textarea(el_val_t attrs, el_val_t value);
@@ -176,43 +177,43 @@ el_val_t el_form(el_val_t attrs, el_val_t children) {
return 0;
}
el_val_t el_h1(el_val_t attrs, el_val_t text) {
el_val_t el_h1(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h1>"), el_escape(text)), EL_STR("</h1>"));
return el_str_concat(el_str_concat(EL_STR("<h1>"), children), EL_STR("</h1>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h1 "), attrs), EL_STR(">")), el_escape(text)), EL_STR("</h1>"));
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h1 "), attrs), EL_STR(">")), children), EL_STR("</h1>"));
return 0;
}
el_val_t el_h2(el_val_t attrs, el_val_t text) {
el_val_t el_h2(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h2>"), el_escape(text)), EL_STR("</h2>"));
return el_str_concat(el_str_concat(EL_STR("<h2>"), children), EL_STR("</h2>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h2 "), attrs), EL_STR(">")), el_escape(text)), EL_STR("</h2>"));
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h2 "), attrs), EL_STR(">")), children), EL_STR("</h2>"));
return 0;
}
el_val_t el_h3(el_val_t attrs, el_val_t text) {
el_val_t el_h3(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h3>"), el_escape(text)), EL_STR("</h3>"));
return el_str_concat(el_str_concat(EL_STR("<h3>"), children), EL_STR("</h3>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h3 "), attrs), EL_STR(">")), el_escape(text)), EL_STR("</h3>"));
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h3 "), attrs), EL_STR(">")), children), EL_STR("</h3>"));
return 0;
}
el_val_t el_h4(el_val_t attrs, el_val_t text) {
el_val_t el_h4(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<h4>"), el_escape(text)), EL_STR("</h4>"));
return el_str_concat(el_str_concat(EL_STR("<h4>"), children), EL_STR("</h4>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h4 "), attrs), EL_STR(">")), el_escape(text)), EL_STR("</h4>"));
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<h4 "), attrs), EL_STR(">")), children), EL_STR("</h4>"));
return 0;
}
el_val_t el_button(el_val_t attrs, el_val_t label) {
el_val_t el_button(el_val_t attrs, el_val_t children) {
if (str_eq(attrs, EL_STR(""))) {
return el_str_concat(el_str_concat(EL_STR("<button type=\"button\">"), el_escape(label)), EL_STR("</button>"));
return el_str_concat(el_str_concat(EL_STR("<button type=\"button\">"), children), EL_STR("</button>"));
}
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<button type=\"button\" "), attrs), EL_STR(">")), el_escape(label)), EL_STR("</button>"));
return el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("<button type=\"button\" "), attrs), EL_STR(">")), children), EL_STR("</button>"));
return 0;
}
+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);
}
-4
View File
@@ -16,7 +16,6 @@
// GET /api/founding-count {"sold":N,"total":N,"remaining":N}
// GET /assets/* static files under $LANDING_ROOT/assets/
// GET /brand/* static files under $LANDING_ROOT/assets/brand/
// GET /legal/privacy privacy.html
// GET /<other> 404 JSON
// Path helpers
@@ -119,9 +118,6 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_eq(clean, "/legal/terms") {
return fs_read(root_dir() + "/terms.html")
}
if str_eq(clean, "/legal/privacy") {
return fs_read(root_dir() + "/privacy.html")
}
if str_eq(clean, "/about") {
return fs_read(root_dir() + "/about.html")
}
+26 -2
View File
@@ -98,7 +98,7 @@ fn checkout_page(plan: String, pub_key: String) -> String {
el_li("", "Persistent memory - never resets")
+ el_li("", "Local inference via Ollama (coming)")
+ el_li("", "Bring your own API keys")
+ el_li("", "Plugin marketplace access (coming soon)")
+ el_li("", "3 marketplace plugins included")
+ el_li("", "Core built-in capabilities")
+ el_li("", "2 devices included")
} else {
@@ -345,7 +345,31 @@ fn checkout_page(plan: String, pub_key: String) -> String {
}
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;
+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);
+12 -30
View File
@@ -82,7 +82,6 @@ from styles import { page_open, page_open_seo, page_close }
from about import { about_page }
from founding_badge import { founding_badge, founding_badge_css }
from terms import { terms_page }
from privacy import { privacy_page }
from enterprise_terms import { enterprise_terms_page }
from checkout import { checkout_page }
from safety import { safety }
@@ -583,7 +582,6 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
+ " <url><loc>https://neurontechnologies.ai/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/about</loc><changefreq>monthly</changefreq><priority>0.8</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/legal/terms</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/legal/privacy</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/legal/enterprise-terms</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
+ "</urlset>\n"
return http_response(200, "{\"Content-Type\":\"application/xml; charset=utf-8\"}", sitemap_body)
@@ -607,15 +605,6 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
return "{\"__status__\":404,\"error\":\"not found\"}"
}
// Privacy Policy
if str_eq(path, "/legal/privacy") {
let privacy_path: String = state_get("__privacy_html_file__")
if !str_eq(privacy_path, "") {
return fs_read(privacy_path)
}
return "{\"__status__\":404,\"error\":\"not found\"}"
}
// Enterprise Agreement
if str_eq(path, "/legal/enterprise-terms") {
let ent_path: String = state_get("__enterprise_terms_html_file__")
@@ -648,7 +637,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."
}
@@ -708,23 +697,21 @@ 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: $0 PaymentIntent for age verification (18+ requirement).
// Verifies card is valid. No charge, no capture.
// Note: setup_future_usage cannot be used with amount=0.
if str_eq(plan, "free") {
let free_si_body: String = "automatic_payment_methods[enabled]=true"
+ "&usage=off_session"
let free_pi_body: String = "amount=0"
+ "&currency=usd"
+ "&payment_method_types[]=card"
+ "&metadata[plan]=free"
+ "&metadata[purpose]=age_verification"
let free_si_body = if !str_eq(pi_cus_id, "") { free_si_body + "&customer=" + pi_cus_id } else { free_si_body }
let free_si_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/setup_intents",
free_si_body,
let free_pi_body = if !str_eq(pi_cus_id, "") { free_pi_body + "&customer=" + pi_cus_id } else { free_pi_body }
let free_pi_resp: String = http_post_form_auth(
"https://api.stripe.com/v1/payment_intents",
free_pi_body,
auth_header)
if str_starts_with(free_si_resp, "{") {
let inner: String = str_slice(free_si_resp, 1, str_len(free_si_resp))
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
}
return free_si_resp
return free_pi_resp
}
// Setup-mode path: save payment method, do not charge. Only valid
@@ -2397,15 +2384,10 @@ let ent_terms_html_path: String = src_dir + "/enterprise-terms.html"
fs_write(terms_html_path, terms_page())
fs_write(ent_terms_html_path, enterprise_terms_page())
// Generate privacy policy HTML.
let privacy_html_path: String = src_dir + "/privacy.html"
fs_write(privacy_html_path, privacy_page())
// Register with El HTTP runtime.
state_set("__html_file__", html_path)
state_set("__about_html_file__", about_html_path)
state_set("__terms_html_file__", terms_html_path)
state_set("__privacy_html_file__", privacy_html_path)
state_set("__enterprise_terms_html_file__", ent_terms_html_path)
state_set("__src_dir__", src_dir)
state_set("__stripe_secret_key__", stripe_key)
+1 -1
View File
@@ -77,7 +77,7 @@ fn marketplace_categories() -> String {
el_div(
"class=\"marketplace-categories reveal\" style=\"transition-delay:320ms\"",
marketplace_tags_block("Connectors (rolling out)", connectors) +
marketplace_tags_block("Connectors - day one", connectors) +
marketplace_tags_block("Following launch", following) +
el_div(
"",
+2 -2
View File
@@ -36,7 +36,7 @@ fn pricing_free_features() -> String {
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Local inference via Ollama (coming)")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference included when it launches - Q3 2026")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Unlimited projects")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Plugin marketplace access (coming soon)")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "3 marketplace plugins included")) +
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Core built-in capabilities"))
}
@@ -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(
-145
View File
@@ -1,145 +0,0 @@
// components/privacy.el - Consumer Privacy Policy page.
// Returns complete HTML using the shared page shell from styles.el.
// Mirrors terms.el. Adapted from docs/legal/privacy-policy-companion-DRAFT.md
// (draft dated 2026-07-14). PENDING ATTORNEY SIGN-OFF before publication.
from styles import { page_open_seo, page_close }
from nav import { nav }
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_strong(children: String) -> String
fn privacy_page() -> String {
page_open_seo(
"Privacy Policy — Neuron",
"Read the Neuron Privacy Policy. Neuron is local-first: your memory and conversations stay on your device.",
"/legal/privacy",
"The Neuron Privacy Policy — local-first by design. Your memory and conversations stay on your device.",
"false"
) + nav() + privacy_body() + page_close()
}
fn privacy_section_head(num: String, title: String) -> String {
el_div(
"style=\"display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem\"",
el_span("style=\"font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)\"", "&#167; " + num) +
el_h2("style=\"font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)\"", title)
)
}
fn privacy_p(text: String) -> String {
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem\"", text)
}
fn privacy_p_last(text: String) -> String {
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8\"", text)
}
fn privacy_body() -> String {
let back_link: String = el_div(
"style=\"margin-bottom:3rem\"",
el_a("/", "style=\"font-family:var(--body);font-size:0.75rem;font-weight:500;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy);text-decoration:none\"", "&#8592; Neuron")
)
let page_header: String = el_div(
"style=\"margin-bottom:4rem;border-bottom:1px solid var(--border);padding-bottom:3rem\"",
el_p("class=\"label\" style=\"margin-bottom:1rem\"", "Legal") +
el_h1("style=\"font-family:var(--head);font-size:clamp(2rem,4vw,3rem);font-weight:600;color:var(--t1);margin-bottom:0.75rem;line-height:1.1\"", "Privacy Policy") +
el_p("style=\"font-family:var(--body);font-size:0.875rem;color:var(--t3)\"", "Effective May 1, 2026 &nbsp;&middot;&nbsp; Neuron, LLC")
)
let s1: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("1", "Who We Are") +
privacy_p("Neuron is made by Neuron Technologies / Neuron, LLC (&#34;Neuron,&#34; &#34;we,&#34; &#34;us&#34;). This policy explains what data Neuron handles, where it lives, and what - if anything - leaves your device.") +
privacy_p_last("Neuron is an AI companion with persistent memory. Because it is designed to remember you across time and to feel warm and personal, we hold ourselves to a local-first standard: your relationship with Neuron stays on your machine.")
)
let s2: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("2", "Our Core Principle - Local-First") +
privacy_p_last("Your Neuron memory and knowledge graph are stored " + "<strong style=\"color:var(--t1);font-weight:500\">on your device</strong>" + " (in ~/.neuron) by a local engine running on your own computer. By default, your memories, notes, conversations, documents, and knowledge never leave your device. We cannot see them.")
)
let s3: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("3", "What Data We Handle, and Where It Goes") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Your memory, conversations, and files.</strong> Stored locally on your device. Neuron&#39;s servers do not receive them.") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">AI inference (bring-your-own-key).</strong> To answer you, the Neuron app on your device sends your conversation content directly to the AI provider you choose (for example, Anthropic) using your own API key. This content goes from your machine to that provider; it does not pass through Neuron&#39;s servers, and we do not store or train on it. That provider&#39;s privacy policy and terms govern how they handle it - you are using your own account with them.") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">AI inference (future Neuron-hosted tiers).</strong> If you opt into a paid tier that uses Neuron-hosted models, conversation content needed for inference will be processed on Neuron-operated infrastructure. When that launches, we will not train on it, will not sell it, and will state retention plainly. This is forward-looking and not active today.") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">License validation.</strong> On startup the app contacts our licensing server to verify your license status only. We do not transmit your memory or conversations during validation.") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Account &amp; purchase data.</strong> If you create an account or buy a tier, we (and our payment processor and sign-in providers) process your email, authentication identity, and payment metadata. We do not receive full card numbers.") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Connectors &amp; imports.</strong> When you connect a third-party service (GitHub, Notion, Linear, Google Drive) or import from another tool, you authorize those flows; the third party&#39;s terms govern their side. Imported content and on-device OCR results are stored locally.") +
privacy_p_last("<strong style=\"color:var(--t1);font-weight:500\">Website.</strong> The marketing website uses standard anti-abuse and sign-in tooling (for example, Cloudflare Turnstile and OAuth sign-in). The desktop app does not send usage telemetry for ordinary local use.")
)
let s4: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("4", "What We Do Not Do") +
privacy_p_last("We do " + "<strong style=\"color:var(--t1);font-weight:500\">not</strong>" + " sell your data. We do " + "<strong style=\"color:var(--t1);font-weight:500\">not</strong>" + " train models on your conversations. We do " + "<strong style=\"color:var(--t1);font-weight:500\">not</strong>" + " read your memory or local files.")
)
let s5: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("5", "Safety Features and Crisis Data") +
privacy_p("If Neuron&#39;s safety features are active, distress detection runs on your device. If triggered, Neuron shows you crisis resources (such as 988) and, if you have set a personal safety contact, shows you how to reach that person. " + "<strong style=\"color:var(--t1);font-weight:500\">Neuron does not contact anyone on your behalf - it does not notify your safety contact, 988, 911, or anyone else. There is no automatic escalation.</strong>") +
privacy_p("Your safety contact (a name, relationship, and phone or email you optionally enter) is stored locally on your device in ~/.neuron. It is surfaced only to you, only in a crisis moment, as a reminder of who you trust. It is not transmitted to us.") +
privacy_p_last("Crisis-related conversation content is excluded from Neuron&#39;s persistent memory (&#34;Hard Bell&#34; does not retain it), so distressing moments are not resurfaced to you later. Neuron is not a crisis service and not a medical or clinical provider (see Terms &#167; 11). We process only what is necessary for the feature to function on your device.")
)
let s6: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("6", "Age and Minors") +
privacy_p_last("<strong style=\"color:var(--t1);font-weight:500\">Neuron is intended for adults; you must be 18 or older to use it</strong>" + " (see Terms &#167; 1). The app asks you to confirm your age before use. We do not knowingly collect personal information from anyone under 18. If we learn that someone under 18 has used Neuron, we will take reasonable steps to delete the information we hold and end that use.")
)
let s7: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("7", "Your Rights and Choices") +
privacy_p_last("Most of your data lives on your device and is under your direct control - you can access, correct, export, and delete it yourself. To delete the account, license, or payment records we hold, contact legal@neurontechnologies.ai. Depending on where you live, you may have GDPR or CCPA/CPRA rights, including the right to know, access, delete, correct, and opt out of the &#34;sale&#34; or &#34;sharing&#34; of your personal information - which we do not do.")
)
let s8: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("8", "Data Retention") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Local data</strong>" + " persists on your device until you delete it - you control it directly.") +
privacy_p_last("<strong style=\"color:var(--t1);font-weight:500\">Account, license, and payment records</strong>" + " we hold are retained only as long as needed to provide the service, then deleted on account closure, subject to legal and accounting requirements.")
)
let s9: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("9", "Security") +
privacy_p_last("We use reasonable technical and organizational measures to protect the limited data we hold (account, license, and payment metadata). Because your memory and conversations stay on your device, the security of that device is also important - keep it protected.")
)
let s10: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("10", "Changes to This Policy") +
privacy_p_last("We will post material changes here and, where appropriate, notify you in-app.")
)
let s11: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("11", "Contact") +
privacy_p_last("legal@neurontechnologies.ai - Neuron Technologies / Neuron, LLC.")
)
let footer_links: String = el_div(
"style=\"margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border);display:flex;gap:2rem;flex-wrap:wrap\"",
el_a("/", "style=\"font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none\"", "&larr; Home") +
el_a("/legal/terms", "style=\"font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none\"", "Terms of Service &rarr;")
)
el_div(
"style=\"max-width:720px;margin:0 auto;padding:6rem 2.5rem 8rem\"",
back_link +
page_header +
s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11 +
footer_links
)
}
+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."
)
+18 -7
View File
@@ -59,7 +59,6 @@ fn terms_body() -> String {
"style=\"margin-bottom:3rem\"",
terms_section_head("1", "Using Neuron") +
terms_p("Using Neuron means you&#39;ve read these terms and agree to them. That&#39;s it. No buried consent, no dark patterns. If something here doesn&#39;t sit right with you, email us before using the product - we&#39;ll talk through it.") +
terms_p("<strong style=\"color:var(--t1);font-weight:500\">You must be 18 or older to use Neuron.</strong> Neuron is intended for adults. By using it you represent that you are at least 18 years old and able to form a binding contract. The app asks you to confirm your age before you can use it, and we do not knowingly permit anyone under 18 to use Neuron.") +
terms_p_last("These terms cover every version of Neuron: free, Professional, and Founding Member. Enterprise use has its own agreement. If you&#39;re deploying Neuron for an organization, that&#39;s the one you want.")
)
@@ -137,14 +136,26 @@ fn terms_body() -> String {
terms_p_last("Founding Member licenses are locked. I won&#39;t retroactively change what you signed up for.")
)
let hard_bell_block: String = el_div(
"style=\"margin:1.5rem 0;padding:1.5rem;border-left:3px solid rgba(0,82,160,.35);background:rgba(0,82,160,.04)\"",
el_p("style=\"font-family:var(--body);font-weight:600;font-size:0.875rem;color:var(--t1);margin-bottom:0.875rem;letter-spacing:0.04em\"", "Hard Bell - Required acknowledgment for family accounts") +
terms_p("Federal data shows approximately 89% of child maltreatment is perpetrated by a parent or caregiver (HHS, Child Maltreatment 2023). The person listed as the primary contact is not always a safe contact. This system is built with that reality in mind.") +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.875rem;color:var(--t1);line-height:1.8\"",
"When you create a family account that includes a minor, you are agreeing unconditionally to the following: " +
"<strong>if a safety signal is triggered by a child&#39;s account, the parent or guardian named on the family account will not be the first contact reached.</strong>" +
" Emergency services and independently designated trusted contacts are notified first. This is not a setting that can be changed. By creating a family account, you accept this structure."
)
)
let s11: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("11", "Safety Features") +
terms_p("Neuron is for adults 18 and older (see &#167; 1). It is not a crisis line, not a medical or mental-health service, and not an emergency responder. In an emergency, contact 911 or your local emergency number, or call or text 988 (the Suicide &amp; Crisis Lifeline) directly.") +
terms_p("Neuron includes optional safety features intended to help if you are ever in distress. It is important that you understand exactly what they do and do not do.") +
terms_p("<strong style=\"color:var(--t1);font-weight:500\">What they do.</strong> If Neuron&#39;s on-device distress detection is triggered, Neuron shows you crisis resources - prominently the 988 Suicide &amp; Crisis Lifeline (call, text, or chat) - and, if you have chosen a personal safety contact, it shows you how to reach that person. You remain in control of whether and how you reach out.") +
terms_p("<strong style=\"color:var(--t1);font-weight:500\">What they do not do. Neuron does not contact anyone on your behalf. It does not call, text, email, or otherwise notify your safety contact, 988, 911, or any other person or service. There is no automatic escalation.</strong> The safety contact you set is stored locally on your device and is surfaced only to you, as a reminder of who you trust - Neuron never reaches out to them for you.") +
terms_p_last("These features are provided &#34;AS IS.&#34; Distress detection is imperfect: it may fail to detect distress, may raise false alarms, and must not be relied upon in an emergency. To the maximum extent permitted by law, Neuron, LLC is not liable for any outcome relating to the presence, absence, accuracy, timing, or performance of any safety feature.")
terms_section_head("11", "Children and Family Accounts") +
terms_p("Children 13 and over may use Neuron independently. Children under 13 may only use Neuron as part of a family account established by a parent or legal guardian. By setting up a family account that includes a minor, the parent or guardian provides verifiable consent for the child&#39;s use and accepts these terms on the child&#39;s behalf.") +
terms_p("When a child&#39;s Neuron instance is part of a family account, certain information - including usage activity and relevant context - may be shared from the child&#39;s local instance to the parent&#39;s local instance. This sharing happens device-to-device and does not pass through or get stored on Neuron&#39;s servers.") +
hard_bell_block +
el_p("style=\"font-family:var(--body);font-weight:500;font-size:0.9375rem;color:var(--t1);line-height:1.8;margin-bottom:0.75rem\"", "Mandatory reporting - applies to all accounts.") +
terms_p("By creating any Neuron account, you acknowledge and agree that: " + "<strong style=\"color:var(--t1);font-weight:500\">if Neuron detects credible, specific indicators of intended harm to a person - including harm to the account holder themselves - emergency services or relevant authorities may be contacted.</strong>" + " This is not limited to family accounts. It applies to every user. This is not a privacy violation. It is the only honest answer to what a system that knows you this well is obligated to do when someone is in genuine danger. You cannot opt out of this. If you are unwilling to agree, do not create an account.") +
terms_p_last("Parents control what is visible to them through their local instance and can revoke family account access at any time. If you believe a child is using Neuron outside of a family account, contact legal@neurontechnologies.ai and we will address it promptly.")
)
let s12: String = el_div(