Commit Graph

8 Commits

Author SHA1 Message Date
will.anderson 7f88414b40 Make migration policy creation idempotent
DROP POLICY IF EXISTS before CREATE POLICY so migrations can be
re-applied to a DB that already has the policy (e.g. demo_config
was manually applied before migration tracking was set up).
2026-05-11 13:56:12 -05:00
will.anderson 18350761c5 Add API key provisioning to accounts page 2026-05-11 12:24:05 -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 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 ff054b9980 fix(gallery): render answer_html so card previews match the share page
Gallery was reading the plain answer field and HTML-escaping it,
showing literal `&lt;ol&gt;...` text where the actual share page rendered
the markdown. Now selects answer_html alongside, runs it through
the same sanitizer as the share-card render, and falls back to
escaped plaintext for legacy rows.
2026-05-02 12:38:48 -05:00
Will Anderson 79cd461b83 feat: phase 1 of runtime config store (neuron_config table, chat.model)
New Supabase table neuron_config keyed on (key, scope) with jsonb
value column. Web tier reads chat.model via /api/demo with 60s TTL
caching, passes to soul via dharma envelope payload.model. No more
revision-rollout-per-model-swap.

Admin read endpoint at /api/admin/config gated by NEURON_ADMIN_TOKEN.
Write surface and Realtime subscription land in Phase 2.

Backlog: bl-6eb51893
2026-05-02 12:24:00 -05:00
Will Anderson 7f1fe1347a add docuseal webhook receiver with event log + completion notification
POST /api/docuseal/webhook/<token> validates the path token against
DOCUSEAL_WEBHOOK_TOKEN, persists every event to docuseal_events with
the full payload as jsonb, and emails Will via Resend on form.completed
or form.declined. Token rotates via Secret Manager.
2026-05-02 12:17:13 -05:00
Will Anderson e121038382 fix(gallery): proper auth-gated voting with persistence, undo, and change
Replaces the broken counter-bump RPC with a per-user share_votes table
(PK share_id+user_id, RLS-enforced ownership). One vote per user per
card, change direction or undo any time. Auth required for write;
read is public. share_cards.upvotes/downvotes/score stay in sync via
recalc trigger. New endpoints: POST /api/vote (auth-gated), GET
/api/vote-state/:id (auth-aware).
2026-05-02 12:14:31 -05:00