Commit Graph

6 Commits

Author SHA1 Message Date
Ben Stull bd3ef269d4 Release v0.27.0: security hardening (audit 0026)
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>
2026-05-28 18:28:10 -07:00
Ben Stull bada72f87e v0.23.0: server-side sign-in state resume (roadmap #29)
Track each authenticated user's last-viewed route + light view state
server-side, and on next sign-in redirect them to that state, falling
back to the empty-state home only when there's no recorded state.

Backend:
- migration 022_user_session_state.sql: one row per user
  (user_id PK/FK, last_route, last_route_state JSON-as-TEXT,
  resume_enabled default 1, last_updated_at).
- PUT /api/me/last-state (require_user): upserts route + light state;
  no-ops when resume_enabled=0. Localized in the /me region.
- /api/auth/me payload now carries resume_enabled + last_route +
  decoded last_route_state (no extra round-trip).
- test_session_resume_vertical.py: auth-required, upsert/read-back,
  per-user isolation, resume_enabled=0 disable.

Frontend:
- lib/useLastState.js: debounced (~1s) route-change PUT for
  authenticated users; one-time resume redirect on sign-in, gated on
  identify having fired (preserves #21 Part C identify-then-track).
- api.js: putLastState() client call.
- App.jsx: import + call the hook; set identifyReady after identify.
  Header region untouched.

Per-user (not per-device); profile-settings opt-out toggle UI
deferred (column + default-on behavior ship now). Stored state is
route + light view state ONLY, never draft buffers — documented in
SPEC §6.8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 12:34:19 -07:00
Ben Stull 822f4266f6 v0.19.0 frontend: /docs/* route tree + flyout nav + sessions browser
Reorganizes the /docs surface from a single DOCS.md route into a hub
with a left-side flyout nav and three new sub-routes for the on-site
sessions browser (roadmap item #30):

  /docs                      → redirect to /docs/user-guide
  /docs/user-guide           → existing DOCS.md content
  /docs/sessions             → redirect to /docs/sessions/about
  /docs/sessions/about       → README.md of ohm-session-history
  /docs/sessions/<NNNN>      → per-session transcript index
  /docs/sessions/<NNNN>/<f>  → per-transcript view

The flyout is a persistent left sidebar on desktop and a slide-out
drawer on mobile (toggled by a ☰ button in the docs header). Its
session list is driven by the /api/docs/sessions/manifest fetch —
loading shows a skeleton; 502 shows an inline retry; empty manifest
shows only the "About" row.

Each sub-route owns its own empty-state / error handling:
  - 404 transcripts render "This transcript isn't published yet"
    with a link back to the parent session index, no JS crash.
  - 502 (gitea unreachable) renders a retry button.
  - Manifest 404 is mapped to {} server-side so the flyout renders
    cleanly with no error banner when no sessions are published yet.

Analytics (per SPEC §21):
  - new EVENTS.DOC_VIEWED ("Doc Viewed") fires on each sub-route
    mount with `section`: 'user-guide' | 'sessions/about' |
    'sessions/<NNNN>' | 'sessions/<NNNN>/<filename>'.
  - every interactive nav element carries aria-label +
    data-amp-track-name so autocapture rows are readable.

The existing v0.14.0 Docs.jsx component is dropped — its content
moved verbatim into DocsUserGuide.jsx; the new layout subsumes the
back-button + signed-out home affordances it used to carry. All
four new sub-routes reuse the existing MarkdownPreview renderer
(marked + mermaid lazy-load) so no second markdown library lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:07:20 -07:00
Ben Stull ee4925b6ac Release 0.16.0: owner-only invite for per-RFC contribution + discussion (+ #21 Part C Amplitude wiring)
Wave 5 / Track B. Roadmap item #12. Folds in #21 Part C Amplitude
wiring inline (operator ask: "best practices from the very get-go").

RFC owners can invite specific users to one of two per-RFC roles:
contributor (open PRs + join discussion) or discussant (discussion
only). Non-invited users keep the v0.6.0 anonymous-read contract.
The per-RFC write gate layers on top of the existing
require_contributor gate; a super-draft with no owners yet falls
through to the platform-granted contract, preserving the
v0.6.0/v0.7.0/v0.8.0 contracts in their domains.

Backend: migration 018_rfc_invitations.sql (auto-applied — two
tables: rfc_invitations + rfc_collaborators); api_invitations.py
with five endpoints + transactional email; auth.py helpers
(is_rfc_owner / is_rfc_collaborator / can_discuss_rfc /
can_contribute_to_rfc / can_invite_to_rfc); api_discussion +
api_branches + api_prs gate composition; api_admin.py additive
rfc_invitations[] per user. 237 backend tests pass (18 new in
test_rfc_invitations_vertical.py).

Frontend: InvitationsModal.jsx (owner surface), AcceptInvitation.jsx
(/invitations/accept route), api.js helpers, RFCView.jsx
Invitations button, App.jsx route registration.

Amplitude wiring (inline, #21 Part C):
  - INVITATION_SENT from InvitationsModal { rfc_slug, role_in_rfc }
  - INVITATION_ACCEPTED from AcceptInvitation { rfc_slug, role_in_rfc }
  - identify() BEFORE the accept event with properties: invited_at
    (setOnce), last_invited_to_rfc, last_invite_role_in_rfc,
    claim_method: 'rfc-invite'
  - EVENTS taxonomy extended with INVITATION_SENT + INVITATION_ACCEPTED

No new secrets, no new overlay keys, no operator gesture beyond the
v0.15.0 overlay-set + restart. Frontend build verified green.

Subagent ν shipped the feature on feature/v0.16.0-owner-invite
(a51beec). Driver-side integration squash-merged into main,
hand-resolved VERSION + package.json + CHANGELOG (strict-descending
0.16.0 → 0.15.0), and added the inline Amplitude wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 05:06:55 -07:00
Ben Stull 72f8457933 Release 0.15.0: Amplitude Analytics + Session Replay (with #21 Part C identity lifecycle)
Wave 5 / Track A. Roadmap item #13. Folds in #21 Part C user-identity-
lifecycle work inline rather than as a follow-up release (operator
ask: "implement Amplitude best practices from the very get-go").

Wraps @amplitude/unified with:
  - consent-gated lazy init (v0.13.0 cookie banner; SDK never loads
    without explicit analytics opt-in)
  - amplitude.initAll(KEY, { analytics: { autocapture: true },
    sessionReplay: { sampleRate: 1 } }) — vendor-recommended shape
  - identify({ user_id, properties? }) with set vs setOnce semantics
  - setUserProperties(properties) for mid-session state changes
  - anonymize() clears both user_id binding AND property cache

Wired into App.jsx (identify on me.user with role / permission_state /
passcode_set / device_trusted as set; first_sign_in_at / account_created_at
as setOnce; Page Viewed on route change; Sign-out fires User Signed
Out + anonymize), Login.jsx (User Signed In + Beta Access Requested),
and the per-feature surfaces (ProposeModal, PRModal, RFCView,
RFCDiscussionPanel, PRView, Admin).

Binding: VITE_AMPLITUDE_API_KEY via `flotilla overlay set`
(bundle-embedded public key like VITE_TURNSTILE_SITE_KEY — not
secret). Mid-Session-L correction: original dispatch brief specified
secret-set; vendor guidance + bundle visibility settled it as overlay.

PII discipline: no email, no display_name, no gitea_login, no free-text
fields ever sent. Properties are opaque ids + enums + timestamps +
booleans only.

Subagent ξ shipped the wrapper structure + nine-event taxonomy on
feature/v0.15.0-amplitude (0fd8c52 + 6cfbf69 post-correction).
Driver-side integration extended the wrapper with setUserProperties
+ identify properties for the #21 Part C identity-lifecycle work.
Frontend build verified green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 05:04:10 -07:00
Ben Stull 8aa65014b4 Release 0.13.0: cookie/privacy consent banner + policy pages (instrumentation prep)
Roadmap item #11. Ships the non-modal bottom-of-page cookie consent
banner, the default /privacy and /cookies policy pages, the
`cookie_consent` table + two §17 endpoints for server-side persistence,
the localStorage fallback for anonymous viewers, the /settings
"Privacy & cookies" tab for revisiting the choice, and the
`frontend/src/lib/consent.js` helper that roadmap item #13's analytics
SDK (v0.15.0) will gate against. No analytics SDK ships in this release
— the consent infrastructure goes in first so the gate is already in
place. Adds SPEC §14.5 / §14.6, lists two new endpoints in §17, names
the new table in §5, and surfaces four §19.2 candidates (content-repo
file vs env-var policy, GPC / DNT headers, i18n, item-#13 dependency).
Two new optional env vars (`VITE_PRIVACY_POLICY_URL`,
`VITE_COOKIES_POLICY_URL`) — defaults render the framework's stub
pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:08:23 -07:00