Commit Graph

27 Commits

Author SHA1 Message Date
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 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 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 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
will.anderson f22d90ac6f Make Free and Professional pricing buttons solid blue
All three pricing CTA buttons now share the same solid navy background,
white text, and blue hover state. Previously only anchor-element rules
existed for the solid variant; the button elements had no explicit
background so all three appeared unstyled.
2026-05-11 12:19:19 -05:00
will.anderson c966f2b455 implement free plan age verification via Stripe SetupIntent; personalize soul demo greeting with user name and timezone
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m49s
2026-05-11 02:03:39 -05:00
will.anderson 43e1245306 seo: full audit fixes — meta, og, schema, canonical, sitemap, headings, alts
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m57s
- Per-page title/description/canonical/OG tags: about, checkout (per-plan),
  terms, enterprise-terms, success all get unique SEO blocks
- Homepage title updated to em-dash form; meta description adds CTA
- og:site_name added to all pages
- noindex/nofollow on checkout, success, account pages
- Sitemap (/sitemap.xml) with all public pages; robots.txt updated with
  Sitemap directive and Disallow for private paths
- Schema: WebSite type added, Organization gains logo ImageObject, SoftwareApplication
  gains url field, billingIncrement corrected to billingPeriod (ISO 8601 P1M),
  sameAs gains x.com/neurontechai alongside GitHub
- marked.min.js given defer attribute (was render-blocking)
- page_head refactored into page_head_base + page_seo_block + page_open_seo
  for clean inner-page overrides without duplicating the CSS/script block
2026-05-10 23:56:40 -05:00
will.anderson fe418bf3f7 feat: auth-gate demo chat + budget circuit breaker
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m10s
Gate the demo chat behind Supabase auth: the widget now fetches Supabase
config on open, shows a compact sign-in pane (Google OAuth or email/password)
when the user is unauthenticated, and passes the access_token to /api/demo.
The server verifies the token via supabase_auth_user() before any processing
and uses the verified user ID as the rate-limit key.

Add a budget kill switch: a demo_config table in Supabase holds a
demo_enabled flag that /api/demo polls every 60s (cached, fails open).
A Cloud Function (demo-budget-guard) is triggered by a GCP Pub/Sub budget
alert and sets demo_enabled = 'false' when spend crosses 90% of the $150
daily budget. Budget and topic are provisioned; function is live in
us-central1.
2026-05-10 23:44:54 -05:00
will.anderson 93f9ea2be2 feat: extract soul-demo into standalone Cloud Run service
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m19s
2026-05-10 21:08:46 -05:00
will.anderson cd1c6737e8 Replace k3s with direct soul-demo watchdog in Cloud Run container
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 2m11s
Cloud Run gen2 doesn't provide eth0 with a unicast IP, causing k3s flannel
to crash on every container start. k3s was also wrong architecture for
Cloud Run (HPA inside a container, k3s overhead for one process).

Changes:
- entrypoint.sh: replace k3s server with a bash watchdog loop that starts
  soul-demo directly and restarts it on crash (3s backoff)
- Dockerfile.stage: remove k3s binary, soul-demo-image.tar, k3s manifests
  and their associated dirs/envvars; keep soul-demo binary only
- stage.yaml: remove 'Download k3s binary' step; rename and simplify
  soul-demo build step to compile binary only (no OCI image/tar)
- dev.yaml: update soul-demo placeholder step (binary not tar)
- manifest.el: document HAVE_CURL requirement since manifest.el has no
  c_flags/link_flags directive support
2026-05-10 19:46:35 -05:00
will.anderson 9a6f0defd1 Fix http handler not found: pre-register via el_runtime_register_handler
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m54s
elb links without -rdynamic so dlsym(RTLD_DEFAULT, "handle_request")
returns NULL at runtime. http_set_handler stores the name as active but
never finds a function pointer, causing every request to return
"el-runtime: no http handler registered" even after http_serve is called.

Fix: add a __attribute__((constructor)) in web_stubs.c that calls
el_runtime_register_handler("handle_request", handle_request) directly,
bypassing dlsym entirely. The handler is in the registry before main()
runs, so http_lookup_active() finds it on the first request.
2026-05-10 13:36:05 -05:00
will.anderson 180acc92a0 Non-blocking entrypoint: start neuron-web before k3s is ready
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m11s
k3s fails to start in Cloud Run gen2 with "unable to select an IP from
default routes" because Cloud Run's network sandbox doesn't expose a
standard default route for k3s to detect. The blocking wait on k3s
prevented neuron-web from ever binding port 8080, causing Cloud Run's
startup probe to time out and terminate the container.

Two changes:
1. Add --flannel-iface=eth0 so k3s pins to Cloud Run's eth0 rather than
   walking the routing table to detect a default-route interface.
2. Start neuron-web immediately after launching k3s in background.
   soul-demo becomes available asynchronously; neuron-web handles it
   being temporarily unavailable gracefully.
2026-05-10 12:54:26 -05:00
will.anderson 9892d89c01 Fix implicit declaration of page_close on Linux: wrap extern as native El fn
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 3m25s
2026-05-09 17:49:15 -05:00
will.anderson fa65f7783e Split page_css.c EL_STR into 18 chunks via el_str_concat to fix runtime segfault
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 3m15s
2026-05-09 17:27:58 -05:00
will.anderson 1110ff2e8c Add SKIP_K3S escape hatch for dev CI smoke test
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 2m21s
k3s requires kernel capabilities (overlayfs) that aren't available in
the CI runner's unprivileged Docker environment. Entrypoint now checks
SKIP_K3S=1 and starts neuron-web directly, bypassing k3s and soul-demo.
Dev CI smoke test sets this flag — prod images are unaffected.
2026-05-09 16:22:40 -05:00
will.anderson 90609c7aaf Convert page_open to native El; fix corrupted CSS
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m57s
elc's heredoc tokenizer was corrupting the inline CSS:
- #FAFAF8 -> FAFAF8 (# treated as comment character)
- 'Playfair Display' -> PlayfairDisplay (quotes + space stripped)
- padding: 0 2.5rem -> padding:02.5rem (spaces between tokens stripped)

The CSS and other complex head content (GA script, JSON-LD schema)
have been pre-compiled to C functions (page_css, page_ga_script,
page_schema) so they bypass the tokenizer entirely and are stored as
properly-escaped C string literals.

page_head() now assembles the <head> content using el-html vessel
calls (el_meta_charset, el_meta, el_title, el_link_stylesheet, etc.)
plus string literals for the vessel gaps. page_open() returns the
complete document prologue as a string concatenation with no heredocs.

page_close() remains pre-compiled in dist/page_close.c (unchanged).
2026-05-09 13:07:06 -05:00
will.anderson 7f27f4be9f Fix broken payment page: escape html/body heredocs in page_open
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 1m27s
elc's heredoc parser treats <html> as an opener and scans forward for
</html>, which exists inside page_close's return statement. This caused
the entire El source of page_close to be injected verbatim into the
page_open output string, terminating the document before Stripe scripts
could load.

Fix: put <!DOCTYPE html><html lang="en"> in a quoted string literal
and use <head>...</head> as the sole heredoc in page_open — closes
within the same function, no cross-boundary scanning. Stub page_close
in styles.el as extern fn so dist/page_close.c supplies the definition.

Also fix elc-broken hyphenated attributes in dist/page_close.c:
aria-label, stroke-width, stroke-linecap, &times;, and several
text nodes that had whitespace stripped by the heredoc parser.
2026-05-09 12:56:50 -05:00
will.anderson c6ee45a374 fix: run k3s as root, bump HPA CPU threshold to 80%
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 3m54s
k3s needs CAP_SYS_ADMIN to create network namespaces and mount cgroups.
USER landing was preventing this. Cloud Run gen2 is the security boundary.

60% CPU was too conservative for soul-demo — it is I/O-bound (LLM API calls),
not CPU-bound. 80% gives correct headroom before scaling kicks in.
2026-05-09 12:40:27 -05:00
will.anderson ddbb568f1d feat: embed k3s in neuron-web image to run soul-demo as managed pods
soul-demo now runs as a k3s Deployment with HPA (1–8 replicas, 60% CPU
target) instead of a bare background process. k3s starts first in
entrypoint.sh, imports the soul-demo:local OCI tar from
/var/lib/rancher/k3s/agent/images, and auto-applies the Deployment,
NodePort Service, and HPA from the server/manifests dir. neuron-web
starts only after the soul-demo pod is Running. Cloud Run gen2 execution
environment required for k3s (provides /dev/kmsg and Linux capabilities).
2026-05-09 12:40:27 -05:00
will.anderson f2ab12e65b feat(native-el-ui): rewrite checkout/main to use el-html vessel, fix page_close linker error
- checkout.el and main.el: replace raw import of el-html vessel with direct
  extern fn declarations; implementations come from dist/elhtml_impl.c (c_source)
- Add src/elhtml.el as reference file (all el-html extern fn declarations; not imported)
- dist/elhtml_impl.c: pre-compiled el-html vessel C (strips int main + sample global)
- dist/page_close.c: pre-compiled page_close implementation; elc OOMs after emitting
  the ~71KB page_open body and silently drops page_close, so supply it as c_source
- manifest.el: add elhtml_impl.c and page_close.c as c_source entries
- .gitignore: un-ignore dist/elhtml_impl.c and dist/page_close.c
2026-05-08 22:14:38 -05:00
will.anderson 2a3f998827 fix(build): c_source stubs, manifest directives, gallery module-level global
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 13m24s
Add window/neuronCheckoutFree stubs to web_stubs.c — needed to link
without undefined symbol errors on macOS (vessel stubs were already
handled, web platform stubs were not).

Add c_source directives to manifest.el so elb includes web_stubs.c and
vessel_stubs.c in the link — requires the matching elb update in
foundation/el PR #46.

Move gallery_share_allowlist from module scope into gallery_page() to
prevent elc from emitting a second main() for the gallery module, which
caused a duplicate-symbol link error when combined with main.el.

Update elc-linux-amd64 binary (rebuilt with RBrace fix from PR #46).
2026-05-07 17:58:58 -05:00
will.anderson e6d10fc3d5 fix(demo): remove 'launch night' from opening greeting — no longer accurate
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 44s
2026-05-07 02:36:10 -05:00
will.anderson 0202b09d37 fix: rebuild soul-demo.c from source, compile against el_runtime.c directly
soul-demo.c was previously an older compiled artifact that triggered an
undefined reference to emit_metric/emit_log/trace_span_* at link time in CI.

Two fixes:
1. Rebuild soul-demo.c from soul-demo.el using current elc — cleaner
   codegen (no double-wrapped el_from_float), same logic, unix_timestamp
   collision with el_runtime.c removed.
2. Dockerfile.stage: compile soul-demo against el_runtime.c directly
   (not el_runtime.o) so all runtime symbols are always resolved from the
   staged source, bypassing any Docker layer cache divergence on el_runtime.o.
2026-05-06 21:30:25 -05:00
Will Anderson afcd03ea31 soul-demo.c: ship the history-aware Claude call source
Deploy marketing to Cloud Run / deploy (push) Failing after 51m19s
Compiled artifact for the soul-history fix that landed in prod
on tag fix-soul-history-1314. Source was built but the .c artifact
didn't get committed alongside the binary deploy. This is the
canonical compiled output of dist/soul-demo.el at HEAD; required
for clean rebuilds via Dockerfile.stage.
2026-05-02 13:21:31 -05:00
Will Anderson 46f93fd6eb security: replace denylist sanitize_share_html with allowlist el_html_sanitize
Deploy marketing to Cloud Run / deploy (push) Failing after 5s
A real attacker probed /api/share earlier today with <script>alert(1),
<iframe src=evil>, <img onerror>, <a href="javascript:...">, and a
<form action="/steal"> payload. Nothing executed because the chat
bubble at /share/<id> renders the served HTML inside marked.js's
already-escaped output, but the prior denylist sanitizer was fragile:

  - It comment-wrapped dangerous tags ("<!--script>...-->") which a
    literal "-->" inside an attacker-supplied attribute value can close
    early, re-exposing the original payload.
  - It renamed on*= attributes to data-x-on*= which left attack
    indicators visible in the served HTML.
  - It was a denylist; every new attack vector required a code change.
  - It didn't validate <a href> URL schemes properly.

The replacement is a runtime-level state-machine allowlist parser
(foundation/el af480f6: el_html_sanitize). The product just specifies
the JSON allowlist of allowed tags + attributes; the runtime drops
everything else, validates href/src URL schemes (http/https/mailto/
fragment/relative only), and drops whole subtrees of script/style/
iframe/object/embed/form regardless of the allowlist.

Phase 4 of bl-dc55ae07: deletes sanitize_share_html (main.el) and
gal_sanitize_html (gallery.el); replaces 3 call sites with
el_html_sanitize(html, allowlist). Defines default_share_allowlist
in main.el and the identical gallery_share_allowlist in gallery.el
(separate bindings to avoid a forward-reference at build-concat
order — gallery is concatenated before main).

Phase 5: migrations/20260502185500_backfill_resanitize_share_cards.sql
nulls answer_html for any share_cards row older than 1 hour. Applied
via the Supabase Management API; 0 rows in scope (the column was
added today and existing rows pre-date its first write).

Also fixes an orthogonal duplicate-symbol bug: unix_timestamp() was
defined in both dist/web_stubs.c and the runtime (the latter is a
recent runtime addition picked up by the runtime sync). Removed the
stub.

Backlog: bl-dc55ae07
2026-05-02 12:56:33 -05:00
Will Anderson 42f8602457 fix: resend email send path - http_post_auth was dropping silently
The wrapper now logs the response and returns a structured ok/error
shape. Four call sites converge on a single send_email helper.
Resend deliveries verified end to end against
will.anderson@neurontechnologies.ai (delivery IDs 492fa066, 74258223,
69a3d9ab, f6d1c889).

Root cause: http_post_auth in dist/web_stubs.c only set the
Authorization: Bearer header. Resend rejects requests without
Content-Type: application/json with HTTP 422 missing_required_field
because it parses the body as form-urlencoded. The 422 response was
being captured by the El handler but not parsed, so callers logged
the error body and returned ok-200 to the client. Two endpoints also
built malformed JSON by interpolating the raw request body unquoted
into the text field.

Fix:
- Added http_post_auth_json (Bearer + Content-Type: application/json)
  alongside http_post_auth in dist/web_stubs.c. Stripe form-POST
  callers stay on http_post_auth, JSON callers (Resend now, others
  later) move to the json variant.
- New send_email(from_addr, to, subject, html, text) wrapper in
  src/main.el. JSON-escapes all user-provided fields, parses the
  Resend response into a structured ok/error envelope, and println's
  the outcome ([email] sent id=<id>) for Cloud Run log surfaces.
- Refactored four call sites onto the wrapper: /api/enterprise-inquiry,
  /api/developer-interest, /api/waitlist, /api/attest, the family
  invite branch in /api/family/invite, and both DocuSeal completion
  branches in /api/docuseal/webhook/<token>.
- Untracked dist/ source files (web_stubs.c, vessel_stubs.c,
  soul-demo.c, entrypoint.sh, engram-snapshot.json) are now committed
  - generated artifacts (main.c, binaries) stay ignored. Without this
  the next CI rebuild would regress the fix.
2026-05-02 12:37:54 -05:00