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.
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.
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
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.