Skip to content

🚀 Release: master ⬅️ staging#95

Merged
mathix420 merged 11 commits into
masterfrom
staging
Jul 13, 2026
Merged

🚀 Release: master ⬅️ staging#95
mathix420 merged 11 commits into
masterfrom
staging

Conversation

@communityfix

@communityfix communityfix Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Release PR from staging to master.

  • chore(security): suppress fallow complexity re-attribution on createCaseStudy
  • 🔒 fix(security): harden links, email-PIN, and AI moderation

mathix420 and others added 3 commits July 2, 2026 14:05
Address three findings from a security audit of the branch:

- XSS (stored): user link/source URLs were rendered straight onto <a :href>
  with only a trim(). sanitizeLinks() now scheme-validates via new URL()
  (http/https/mailto only), case-study `sources` are routed through it (they
  bypassed it entirely), and a safeUrl() render guard backs the three link
  render sites as defense-in-depth for any pre-existing data.

- Email-PIN abuse: /_auth/email-pin/send was unauthenticated with no HTTP
  rate limit, allowing a mail-flood/spam relay across arbitrary addresses.
  Added per-IP + per-email limits on send, and per-IP + per-email limits on
  verify so PIN brute force is bounded at the HTTP layer, not just by the
  non-atomic per-code attempt counter.

- Moderation prompt injection: user content is now fenced in <submission>
  tags with a system caveat telling the model to treat it as untrusted data;
  stripFenceTokens() prevents forging the boundary; model-suggested new tags
  are bounded (<=3, <=40 chars, no empty slugs) so injection can't pollute the
  global taxonomy. Bumped issue.moderate / case-study.moderate prompt versions
  and updated the golden snapshots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aseStudy

The diff-gate flagged createCaseStudy as an introduced complexity finding only
because this PR touches it to route `sources` through sanitizeLinks (net-neutral
to its pre-existing critical complexity). Scoped fallow-ignore with rationale;
the finding is legacy debt, not introduced by this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…it-fixes

🔒 Security: harden links (XSS), email-PIN rate limits, and AI moderation injection
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🧹 Code quality

Check Summary
vize lint 0 errors · 342 warnings
vize format all files formatted
fallow audit fail · 0 dead-code · 119 complexity · 0 dupes (169 files)
fallow audit output
  server/routes/auth/google.get.ts
    :2 onSuccess
           5 cyclomatic    4 cognitive   13 lines
          30.0 CRAP
  server/api/admin/issues/[id]/reject.post.ts
    :5 default
           5 cyclomatic    4 cognitive   44 lines
          30.0 CRAP
  app/pages/dashboard.vue
    :29 firstName
           5 cyclomatic    1 cognitive    1 lines
          30.0 CRAP
  server/api/issue/[id]/members.post.ts
    :9 default
           5 cyclomatic    2 cognitive   23 lines
          30.0 CRAP
  app/pages/onboarding.vue
    :56 goTo
           5 cyclomatic    3 cognitive    5 lines
          30.0 CRAP
    :98 fetchErrorMessage
           5 cyclomatic    1 cognitive    4 lines
          30.0 CRAP
  app/components/user/InterestsEditor.vue
    :63 fetchErrorMessage
           5 cyclomatic    1 cognitive    4 lines
          30.0 CRAP
  app/pages/settings.vue
    :8 fetchErrorMessage
           5 cyclomatic    1 cognitive    4 lines
          30.0 CRAP
  Functions and synthetic template or component entries exceeding cyclomatic, cognitive, or CRAP thresholds (https://docs.fallow.tools/explanations/health#complexity-metrics)
  To suppress inline templates: // fallow-ignore-next-line complexity (above @Component)
  To suppress: // fallow-ignore-next-line complexity

✗ 119 above threshold · 879 analyzed (0.01s)
note: skipped 26 files matching default duplicates ignores (use --explain-skipped for the list)
note: hid 32 clone groups below minOccurrences=3 (lower --min-occurrences to see them)
✗ complexity: 119 findings · 168 changed files (0.50s)
  audit gate excluded 79 inherited findings (run with --gate all to enforce)

updated for f676c7b · run #275

claude and others added 7 commits July 10, 2026 16:45
- Move the parent "Case study of" callout below the title and badges,
  matching how issues render (id → title → badges → parent).
- Hide the owner/collaborators panel behind a dedicated Contributors
  page and the change log behind a History page, reached via quiet meta
  links at the bottom of the overview — the same pattern issues and
  solutions use. Both are now nested routes under /case-study/[id].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MFHDAHHvfjqmUf7TycaJJ
- Extract the outcome/scale badge label+variant maps — previously
  copy-pasted across the case-study page, case-study card, tree node,
  and issue overview — into a single app/utils/case-study.ts.
- Extract the shared "Contributors · History" overview footer into a
  NodeMetaLinks component, used by both the issue and case-study
  overviews, and the meta-page "← back" link into NodeBackLink.

No visual change; purely removes duplication.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MFHDAHHvfjqmUf7TycaJJ
- Pin vue-tsc (2.2.10) in devDependencies. CI runs `bunx vue-tsc`, which
  otherwise resolves vue-tsc@latest (v3) — incompatible with the pinned
  TypeScript on Node 24 (ERR_PACKAGE_PATH_NOT_EXPORTED './lib/tsc'),
  failing the required typecheck job repo-wide. A local pin makes bunx use
  a working version.
- Apply vize formatting to the new/changed case-study files.
- Extract case-study date-range + cost formatting into small pure helpers
  in app/utils/case-study.ts (shared, testable, lower per-fn complexity).
- Extract the near-identical Sources/Links lists into CaseStudyLinkCard.
- Flatten history.vue's template (derive viewerId/revisionCount in script).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MFHDAHHvfjqmUf7TycaJJ
GitHub Actions did not schedule a run for the previous commit; empty
commit to fire the pull_request:synchronize workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MFHDAHHvfjqmUf7TycaJJ
…ribution

Resolves the conflict in app/pages/case-study/[id].vue (the refactor turned it
into a <NuxtPage /> shell, so staging's safeUrl() hardening of the sources/links
hrefs from #94 no longer applied there). Kept the shell and re-applied safeUrl()
in the extracted CaseStudyLinkCard component so the security fix survives.

Also scopes fallow-ignore comments on the shell + Overview templates: the
refactor splits one monolithic template into two simpler ones (net complexity
drop) but the diff-gate re-attributes each touched template as introduced —
same pattern as createCaseStudy (bb3550c).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EzhVToTww4v85fHmuWyPEh
…n id

- Remove provide('caseStudyParent') — no child route injects it (the parent
  callout is rendered in the shell itself).
- Key the pending-banner revisions fetch on the case-study id so client
  navigation between two studies can't briefly surface the previous count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EzhVToTww4v85fHmuWyPEh
…p257al

Refactor case study page into nested routes with Overview/History/Contributors
…tter prefs (#97)

* ✨ feat: catalog navigation rework, wanted skills, onboarding + newsletter prefs

Navigation and discovery:
- Rework home page: new hero, linked catalog stat line, catalog-wide
  full-text search (/api/search/quick) with grouped results, topic
  quick-nav chips, explore-card grid into every section
- New browse pages: /issues (tag + SDG + sort filters), /solutions
  (status filter), /case-studies (outcome/scale/semantic filters),
  /tags (usage-ranked topic index), /map (fullscreen map of all
  geolocated content with layer toggles)
- New endpoints: /api/stats, /api/map, /api/solutions,
  /api/search/quick; sdg filter on /api/issues; usage counts on
  /api/tags and /api/sdgs
- Minimal redesign of case-study cards; solution cards drop
  irrelevant sub-issue/solution chips; sitemap + SEO for new pages
- Mobile responsiveness pass across public pages

Community features:
- Wanted skills on issues/solutions: schema + unique index, CRUD
  endpoints, node-page section with social share, contribute card
- Contribute page rework with always-on ways to help
- Onboarding flow (/onboarding): skills, free-text interests with
  embedding-based topic preview (/api/tags/similar), newsletter
  opt-in with frequency + content preferences
- user_interests (vector embeddings + HNSW index), newsletter_prefs,
  users.onboarded_at; settings sections for interests + newsletter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix: typecheck + formatting for CI

Typecheck (vue-tsc):
- Widen useUmami track() data type to accept boolean | undefined so
  event props like { enabled, on, newsletter } and conditional
  { tag: x || undefined } type-check (NewsletterPrefs, map, onboarding,
  issues pages).
- contribute.vue: type help-label counts as Record<string, number> and
  guard LABEL_META[activeLabel]?.hint under noUncheckedIndexedAccess.

Formatting (vize fmt): reformat files flagged by format:check
(admin logs handler wrap + drizzle migration meta trailing newlines).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix: address PR #97 review findings

- Escape user-derived values in revision notification emails (shared
  escapeHtml util, also reused by the OAuth consent page)
- Guard the revision approve compensation and make post-apply
  snapshot/audit writes loud-but-best-effort (never revert an applied
  revision to pending — that would allow a double apply)
- Exclude rejected case studies from the me/overview counter
- Add missing DB CHECK constraints for revisions.ai_verdict,
  revisions.decided_by_role, and newsletter_prefs.frequency
- /api/case-studies now returns { items, degraded } so clients can tell
  when a semantic query fell back to recency; surface it on the page
- Remove forbidden data-umami-event attributes; use useUmami() instead
  of the bare umami global in appeal handlers
- Clear debounce timers on unmount; skip empty-query quick search on
  homepage first paint
- Fix tests: assert the intended most_voted default sort, make
  similar.test.ts actually exercise the endpoint (summary, not
  description), and turn the stale issue-detail moderation-fields test
  into a privacy regression test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ✨ feat: rework onboarding UI/UX

Clickable stepper with sharp progress bars and live count chips,
personalized greeting, icon tiles + ghost step numerals on cards,
tap-to-fill skill examples, back navigation with direction-aware
transitions, ?step= URL sync, auto-add of typed-but-unsubmitted
skills on continue, and a completion panel with next-step CTAs
instead of an instant redirect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ♻️ refactor: extract shared issue list, guard, and counter helpers

Deduplicate the issue/solution list queries (issue-list.ts), route-param
and node-lookup guards (route-guards.ts), and parent counter updates
(issue-counters.ts, shared with the moderation worker), and spread a
common timestamps pair across the schema tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ♻️ refactor: share leaflet zoom-control styling in assets/css/leaflet.css

The three map components carried identical :deep(.leaflet-control-zoom)
overrides; hoist them into one global stylesheet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ✨ feat: newsletter sending pipeline

Weekly/monthly digest emails for opted-in users, sent by cron tasks
(Mondays / 1st of month, 8am UTC):

- newsletter-digest.ts assembles per-run sections (good news, help
  wanted, product updates) once and per-user sections (interest matches
  via pgvector, skill matches vs recent wanted skills) per recipient;
  empty digests are never sent
- newsletter-send.ts mirrors compute:trust-scores (chunked, per-user
  failures isolated) with a lastSentAt column guarding cron retries
- dashboard-bento email template (rounded cards on gray, Oswald caps,
  chips, blue pill button) with a plaintext twin; all user content
  escaped
- HMAC-token unsubscribe endpoint that works logged-out, linked from
  every footer
- sendEmail now works without an h3 event: the cloudflare:scheduled
  hook stashes the EMAIL binding for tasks

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 💄 style: drop duplicated wordmark from homepage hero

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ♻️ refactor: dedupe shared helpers + reformat drizzle meta

Eliminate all 6 fallow clone groups:
- add requireEventContext/loadIssueOr404 to route-guards, shared by the
  5 issue/case-study moderation handlers
- extract formatRelative into app/utils/relative-time.ts (4 call sites)
- extract cleanLinkRows into app/utils/link-rows.ts (6 call sites)

Also reformat the two drizzle-generated migration meta files that were
failing vize fmt --check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 chore: exclude drizzle migrations dir from vize

Drizzle rewrites the snapshot/journal JSON in its own format on every
db:generate, so scanning them just makes a new migration fail fmt --check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@mathix420
mathix420 merged commit eddd5b0 into master Jul 13, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants