Remediates the rfc-app application + deploy-config findings from the
Session 0026 security audit. Cut as the "v0.25.0-security-hardening"
branch (from v0.24.0); reversioned to 0.27.0 on rebase onto main since
v0.26.0 (#28) shipped while this was in flight.
- C1 (Critical): single sanitizeHtml.js chokepoint (DOMPurify) for every
marked→innerHTML / dangerouslySetInnerHTML sink (MarkdownPreview,
ProposalView x2, Editor); rel=noopener hook on target=_blank links.
- H1: per-account OTC-verify lockout (migration 023, auto-applied) +
per-IP throttle via new ratelimit.py; wired on otc verify/request +
passcode check/verify.
- M1: device_trust.lookup() single indexed-row read — cookie value is now
"<row_id>.<raw_token>"; bcrypt-checks one row, not a global table scan.
(Behavior change: existing device-trust cookies re-prompt once.)
- M2: HTTP security headers (CSP/HSTS/XFO/XCTO/Referrer-Policy) at nginx.
- M4: session cookie Secure-by-default (SESSION_COOKIE_SECURE opt-out).
- M5: bounce webhook fails CLOSED (503) when secret unset, instead of open;
RFC_APP_INSECURE_BOUNCE_WEBHOOK=1 dev opt-in.
- L2/L3: per-IP cooldown + check-endpoint throttle.
- L4: systemd sandbox knobs. L8/I1: nginx server_tokens off + TLS1.0/1.1 out.
VERSION + frontend/package.json → 0.27.0; CHANGELOG documents the upgrade
steps (incl. the out-of-band nginx + systemd apply, which the flotilla
deploy gesture does not perform).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap the Contribute-mode editing surface from Tiptap WYSIWYG to a
CodeMirror 6 markdown source editor. Discuss mode and any read-only
viewing continues to render through Tiptap.
The §8.11 manual-edit debounce now reads the raw markdown source via
CodeMirror's doc, eliminating the lossy `editor.getText()` round-trip
that RFCView.jsx flagged as a §19.2 candidate; what the contributor
typed is exactly what gets POSTed to manual flush.
The §8.10 paragraph-margin gutter accent on Tiptap is dropped — it was
dead in read-only Discuss anyway, and Phase 4 of the Contribute
rewrite adds a change-anchored gutter against the CM6 raw pane.
The Tiptap-side accept-time injection of <span class="tracked-*">
markup also drops out — CM6 can't render those spans, and Phase 3's
preview pane is the proper home for tracked changes. The reviewMode /
DiffView toggle still works against marked-rendered HTML in the
interim until Phase 7 retires it.
Notes:
- Bundle gzipped grows ~50KB for CM6 modules (state/view/commands/
language/lang-markdown). Mermaid in Phase 2 will be the larger lift
and should lazy-load.
- Verified the CM6 editor mounts cleanly via Vite dev preview: ref
handle (`view/getDoc/setDoc`) is wired, `onUpdate` fires on doc
changes, gutter / line-numbers / active-line all render, and the
source round-trips verbatim. Could not drive the full RFCView
Contribute flow end-to-end without a running backend; the manual
countdown + save-now + accept/decline pathways are verified by
inspection only.
- 125 backend integration tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per the §19.1 brief: the three-column shape (§8.1) opens on main
in discuss mode (§8.2), supports the §8.3 discuss-vs-contribute
flip on non-main branches, hosts §8.4's per-branch chat with AI
participation (§18's <change> protocol → §8.14 changes rows), the
§8.8 change-card panel with §8.9 accept/decline/edit-before-accept,
the §8.10 tracked-change markup + DiffView toggle, the §8.11
manual-edit flushes with the stale-change mechanic, the §8.12
range and paragraph sub-threads, the §8.13 flag affordance, and
the §8.14 discuss-mode buffer.
Backend: bot.py grew per-RFC-repo write ops (cut_branch_from_main,
commit_accepted_change with the structured original/proposed/reason
body and Change-Id + Source-Message-Id + On-behalf-of trailers,
commit_manual_flush, ensure_rfc_repo_seed). cache.py grew
refresh_rfc_repo and the webhook dispatches on repository.full_name.
providers.py and chat.py port the §18 carryovers — multi-provider
LLM abstraction and SSE-streaming chat against the §5 threads /
thread_messages / changes schema. api_branches.py mounts the §17
branches/<branch>/* and threads/<thread_id>/* routes with the §6
/ §11 permission checks inline.
Frontend: RFCView.jsx rebuilt as the §8 surface; Editor.jsx,
ChatPanel.jsx, ChangePanel.jsx, PromptBar.jsx, SelectionTooltip.jsx,
DiffView.jsx, ModelPicker.jsx, modelStyles.js lifted from the
prototype and adapted to the canonical schema.
Covered by `backend/tests/test_rfc_view_vertical.py` — eleven new
integration tests against an extended FakeGitea (PUT contents,
POST orgs/{org}/repos, seed_rfc_repo): main-view read,
promote-to-branch, accept (with and without edit-before-accept),
decline, manual flush + system message, flag creation, visibility
flip, anonymous read-but-no-contribute, stale-change refusal, and
the chat-streaming path with a fake provider injected. The 5
Slice 1 tests continue to pass alongside.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>