Skip to content

Add GitHub Discussions and Releases support - #103

Merged
livrasand merged 1 commit into
mainfrom
Add-GitHub-Discussions-and-Releases-support
Jul 13, 2026
Merged

Add GitHub Discussions and Releases support#103
livrasand merged 1 commit into
mainfrom
Add-GitHub-Discussions-and-Releases-support

Conversation

@livrasand

@livrasand livrasand commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Adds comprehensive support for GitHub Discussions, including anonymous commenting via GraphQL API. Implements a releases viewer for both GitHub and GitLab repositories with asset downloads. Introduces server-side proxies for GitHub wiki and API endpoints to avoid CORS and rate-limiting issues. Enhances UI with discussion detail views, release information, and improved wiki navigation with anchor support and page caching.

Summary by CodeRabbit

  • New Features
    • Added a Releases view with release notes, version badges, assets, and responsive details.
    • Added browsing for GitHub Discussions, including discussion details, categories, comment counts, and answered status.
    • Added anonymous commenting on GitHub Discussions with inline confirmation.
    • Added proxy support for GitHub Discussions and wiki pages.
  • Improvements
    • Improved wiki navigation, internal links, anchors, and Markdown rendering.
    • GitLab wiki navigation now appears only when wiki pages are available.

Adds comprehensive support for GitHub Discussions, including anonymous commenting via GraphQL API. Implements a releases viewer for both GitHub and GitLab repositories with asset downloads. Introduces server-side proxies for GitHub wiki and API endpoints to avoid CORS and rate-limiting issues. Enhances UI with discussion detail views, release information, and improved wiki navigation with anchor support and page caching.
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds GitHub Discussion proxying and anonymous comments, extends provider APIs, and introduces repository release browsing. The web interface also updates Discussion rendering, wiki navigation, markdown links, caching, and GitLab wiki visibility.

Changes

GitHub Discussions backend

Layer / File(s) Summary
Discussion comment provider contract
internal/provider/provider.go, internal/provider/github/github.go, internal/provider/gitlab/gitlab.go, internal/github/pr.go
Adds anonymous Discussion comment support to providers and implements GitHub GraphQL resolution and comment creation, while GitLab returns an unsupported-operation error.
Discussion and wiki proxy endpoints
internal/http/handlers.go, internal/http/router.go
Adds Discussion list/detail and GitHub wiki proxy handlers with validation and GraphQL/rate-limit error handling, then wires their routes.
Anonymous comment submission flow
internal/http/handlers.go, internal/http/router.go
Validates, moderates, submits, and optionally persists anonymous Discussion comments; adds the endpoint and permits GitHub connections in CSP.

Repository web interface

Layer / File(s) Summary
Release listing and details
web/repo.html
Adds release counts, provider normalization, release lists, release details, badges, assets, syntax highlighting, responsive styling, and navigation.
Discussion loading and rendering
web/repo.html
Routes Discussion loading through server APIs and updates list, detail, timestamp, answered-state, and comment-submission rendering.
Wiki navigation and markdown links
web/repo.html
Adds cached in-panel wiki navigation, anchor handling, wiki-aware markdown rendering, safe attribute escaping, and conditional GitLab wiki visibility.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • livrasand/gitGost#76: Introduced the shared provider architecture extended here for anonymous Discussion comments.

Suggested labels: enhancement

Suggested reviewers: gitgost-anonymous

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main additions: GitHub Discussions and Releases support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Add-GitHub-Discussions-and-Releases-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@livrasand
livrasand merged commit 737683a into main Jul 13, 2026
3 of 4 checks passed
@livrasand
livrasand deleted the Add-GitHub-Discussions-and-Releases-support branch July 13, 2026 19:30

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3920bbe5c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/http/router.go
Comment on lines +323 to +324
api.GET("/gh-discussions/:owner/:repo", GitHubDiscussionsProxyHandler)
api.GET("/gh-discussion/:owner/:repo/:number", GitHubDiscussionDetailProxyHandler)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate public discussion proxies before using GITHUB_TOKEN

These new /api/gh-discussion(s) endpoints are under the unauthenticated public API group, while their handlers attach the server GITHUB_TOKEN to the GitHub GraphQL request. If that token can read any private repository, any caller can fetch private discussion titles, bodies, and comments just by naming owner/repo; add an access check such as verifying the repo is public, or avoid using the server token for this public proxy.

Useful? React with 👍 / 👎.

Comment thread web/repo.html
Comment on lines +3585 to +3587
const link = res.headers.get('Link') || '';
const match = link.match(/&page=(\d+)>; rel="last"/);
const count = match ? parseInt(match[1]) : 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count single-release repositories in the sidebar

When a GitHub repository has exactly one release, the per_page=1 response has no pagination Link header, so count becomes 0 even though releases[0] exists. That hides the Releases sidebar section for single-release projects, leaving the newly added release viewer undiscoverable in that common case; fall back to the returned array length when there is no rel="last" link.

Useful? React with 👍 / 👎.

Comment thread internal/http/handlers.go
}
reportURL := fmt.Sprintf("%s://%s/v1/moderation/report?hash=%s", getScheme(c.Request), c.Request.Host, hash)

legend := fmt.Sprintf("\n\n---\ngoster-%s · karma (%d) · [report](%s)", hash, karma, reportURL)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add discussion moderation before linking reports

For discussion comments this report link sends users into ReportHashHandler, but that moderation path only calls github.UpdateCommentsKarmaByHash/DeleteCommentsByHash, which search and mutate issue/PR REST comments under /issues/{n}/comments. Discussion comments created via GraphQL are never updated or deleted, so abuse reports on a discussion comment appear accepted while the reported comment stays live; add GraphQL discussion-comment moderation support before publishing the report link here.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
internal/github/pr.go (2)

431-495: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

GraphQL queries built via %q string interpolation instead of variables.

%q escapes using Go string-literal rules, not GraphQL/JSON string escaping. Control bytes in user-typed body (e.g. \v, \a) get encoded into escape sequences GraphQL's parser doesn't recognize (\xHH isn't valid GraphQL), so some legitimate comment text will fail to post with an opaque graphql: ... error. Passing discussionId/body as GraphQL variables (JSON-encoded) sidesteps this entirely and is the standard way to safely interpolate user content into a GraphQL-over-HTTP request.

♻️ Proposed fix using GraphQL variables
-	mutation := fmt.Sprintf(`mutation {
-		addDiscussionComment(input: {discussionId: %q, body: %q}) {
-			comment { url }
-		}
-	}`, discussionID, body)
-	mutPayload := map[string]string{"query": mutation}
+	mutation := `mutation($discussionId: ID!, $body: String!) {
+		addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
+			comment { url }
+		}
+	}`
+	mutPayload := map[string]any{
+		"query": mutation,
+		"variables": map[string]string{"discussionId": discussionID, "body": body},
+	}

The same fix applies to the id-resolution query (owner/repo/number via variables instead of %q/%d).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/github/pr.go` around lines 431 - 495, Update
CreateAnonymousDiscussionComment to stop interpolating owner, repo, discussion
number, discussionID, and body into GraphQL strings with fmt.Sprintf; define
GraphQL operations using variable placeholders and send the corresponding values
through JSON-encoded variables in both the discussion ID query and
addDiscussionComment mutation. Preserve the existing request, response decoding,
and error handling behavior while ensuring all user-provided values are encoded
by JSON.

1-1: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

GraphQL queries hand-built via %q string interpolation across three sites — use GraphQL variables instead.

All three sites construct GraphQL request bodies by fmt.Sprintf-ing values directly into the query text using %q/%d. Go's %q escaping (Go string-literal rules) doesn't match GraphQL/JSON string escaping (e.g. \xHH, \a, \v aren't valid GraphQL escapes), so arbitrary text — especially the free-form comment body in pr.go — can produce a syntactically invalid GraphQL document and a confusing failure. The standard fix is to keep the query text static and pass values via a JSON-encoded variables map, which guarantees correct escaping and removes the hand-rolled escaping surface entirely.

  • internal/github/pr.go#L431-495: pass owner/repo/number (id-resolution query) and discussionId/body (mutation) as GraphQL variables instead of interpolating with %q/%d.
  • internal/http/handlers.go#L1509-1523: pass owner/repo as variables in the discussions-list query instead of %q interpolation.
  • internal/http/handlers.go#L1626-1647: pass owner/repo/number as variables in the discussion-detail query instead of %q/%d interpolation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/github/pr.go` at line 1, The GraphQL requests in the PR and HTTP
handler flows interpolate values directly into query strings, causing invalid
escaping and unsafe query construction. Update the id-resolution and comment
mutation logic near the PR query sites, plus the discussions-list and
discussion-detail handlers, to keep query text static and provide owner, repo,
number, discussionId, and body through JSON-encoded GraphQL variables; remove
the corresponding %q/%d interpolation while preserving existing request
behavior.
internal/http/handlers.go (1)

1509-1523: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Same GraphQL string-interpolation pattern as internal/github/pr.go.

owner/repo are interpolated via %q into the query text rather than passed as GraphQL variables. Lower risk here than in pr.go (usernames/repo names are typically restricted character sets), but for consistency and defense-in-depth the same variables-based fix should be applied. See consolidated comment.

Also applies to: 1626-1647

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/http/handlers.go` around lines 1509 - 1523, Update the discussion
GraphQL query and the corresponding query around the later discussion handler to
use GraphQL variables for owner and repository instead of interpolating them
with fmt.Sprintf. Define the variable declarations and references in the query,
then pass owner and repo through the request’s variables payload, matching the
variables-based pattern used in internal/github/pr.go.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/http/handlers.go`:
- Around line 1747-1785: Update GitHubWikiProxyHandler to URL-escape the owner,
repo, and page path segments before constructing pageURLs, ensuring characters
such as #, ?, &, and spaces remain part of the intended wiki path. Preserve the
existing .md fallback and request behavior.
- Around line 1966-2002: Move the karma and cooldown mutations from before the
provider call to after a successful prov.CreateAnonymousDiscussionComment in the
anonymous comment handler, keeping the existing reports-based karma calculation
and blocking checks unchanged. Apply the same ordering change to the
corresponding anonymous comment handlers that use updateKarma and
markFlaggedAction, so provider failures do not persist state.

In `@internal/http/router.go`:
- Around line 323-325: Update the route registrations for
GitHubDiscussionsProxyHandler, GitHubDiscussionDetailProxyHandler, and
GitHubWikiProxyHandler to apply a dedicated per-IP ghProxyLimiter, analogous to
prCheckLimiter, before invoking these public proxy handlers. Preserve the
existing route paths and middleware while ensuring unauthenticated clients
cannot exhaust the shared GitHub token budget; add short-TTL handler caching
only if needed to reduce redundant upstream requests.

In `@web/repo.html`:
- Around line 3583-3605: Update the GitHub releases logic around the count
calculation and the count > 0 && latest guard to fall back to releases.length
when the Link header does not provide a page count. Ensure repositories with one
release display the sidebar and latest release while preserving the existing
Link-header count for multi-release repositories.
- Around line 4118-4139: Sanitize the HTML produced by marked in renderMd before
it reaches any innerHTML assignment, using the existing DOMPurify integration or
an equivalent sanitizer. Apply this at every renderMd output sink for untrusted
issue, discussion, release, and wiki content, while preserving the existing
link-rendering behavior.

---

Nitpick comments:
In `@internal/github/pr.go`:
- Around line 431-495: Update CreateAnonymousDiscussionComment to stop
interpolating owner, repo, discussion number, discussionID, and body into
GraphQL strings with fmt.Sprintf; define GraphQL operations using variable
placeholders and send the corresponding values through JSON-encoded variables in
both the discussion ID query and addDiscussionComment mutation. Preserve the
existing request, response decoding, and error handling behavior while ensuring
all user-provided values are encoded by JSON.
- Line 1: The GraphQL requests in the PR and HTTP handler flows interpolate
values directly into query strings, causing invalid escaping and unsafe query
construction. Update the id-resolution and comment mutation logic near the PR
query sites, plus the discussions-list and discussion-detail handlers, to keep
query text static and provide owner, repo, number, discussionId, and body
through JSON-encoded GraphQL variables; remove the corresponding %q/%d
interpolation while preserving existing request behavior.

In `@internal/http/handlers.go`:
- Around line 1509-1523: Update the discussion GraphQL query and the
corresponding query around the later discussion handler to use GraphQL variables
for owner and repository instead of interpolating them with fmt.Sprintf. Define
the variable declarations and references in the query, then pass owner and repo
through the request’s variables payload, matching the variables-based pattern
used in internal/github/pr.go.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e5447c3d-7f53-480f-bb91-dd3d8bba0a77

📥 Commits

Reviewing files that changed from the base of the PR and between 3022287 and 3920bbe.

📒 Files selected for processing (7)
  • internal/github/pr.go
  • internal/http/handlers.go
  • internal/http/router.go
  • internal/provider/github/github.go
  • internal/provider/gitlab/gitlab.go
  • internal/provider/provider.go
  • web/repo.html

Comment thread internal/http/handlers.go
Comment on lines +1747 to +1785
func GitHubWikiProxyHandler(c *gin.Context) {
owner := c.Param("owner")
repo := c.Param("repo")
page := c.Param("page")
if page == "" {
page = "Home"
}

// intentar con .md primero, luego sin extension
pageURLs := []string{
fmt.Sprintf("https://raw.githubusercontent.com/wiki/%s/%s/%s.md", owner, repo, page),
fmt.Sprintf("https://raw.githubusercontent.com/wiki/%s/%s/%s", owner, repo, page),
}

client := &http.Client{Timeout: 15 * time.Second}
for _, pageURL := range pageURLs {
req, err := http.NewRequest("GET", pageURL, nil)
if err != nil {
continue
}
req.Header.Set("Accept", "text/plain")
req.Header.Set("User-Agent", "gitGost/1.0")

resp, err := client.Do(req)
if err != nil || resp.StatusCode != 200 {
if resp != nil {
resp.Body.Close()
}
continue
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
c.Data(200, "text/plain; charset=utf-8", body)
return
}

c.JSON(http.StatusNotFound, gin.H{"error": "wiki page not found"})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wiki path segments aren't URL-escaped before being embedded into the target URL.

owner, repo, and especially page (a free-form wiki title) are inserted raw into the raw.githubusercontent.com URL. A page title containing #, ?, &, or spaces will truncate/corrupt the constructed URL (fragment/query boundary) or fail to match, silently returning a wrong page or 404 instead of the intended wiki page.

🔧 Proposed fix
 	pageURLs := []string{
-		fmt.Sprintf("https://raw.githubusercontent.com/wiki/%s/%s/%s.md", owner, repo, page),
-		fmt.Sprintf("https://raw.githubusercontent.com/wiki/%s/%s/%s", owner, repo, page),
+		fmt.Sprintf("https://raw.githubusercontent.com/wiki/%s/%s/%s.md", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(page)),
+		fmt.Sprintf("https://raw.githubusercontent.com/wiki/%s/%s/%s", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(page)),
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func GitHubWikiProxyHandler(c *gin.Context) {
owner := c.Param("owner")
repo := c.Param("repo")
page := c.Param("page")
if page == "" {
page = "Home"
}
// intentar con .md primero, luego sin extension
pageURLs := []string{
fmt.Sprintf("https://raw.githubusercontent.com/wiki/%s/%s/%s.md", owner, repo, page),
fmt.Sprintf("https://raw.githubusercontent.com/wiki/%s/%s/%s", owner, repo, page),
}
client := &http.Client{Timeout: 15 * time.Second}
for _, pageURL := range pageURLs {
req, err := http.NewRequest("GET", pageURL, nil)
if err != nil {
continue
}
req.Header.Set("Accept", "text/plain")
req.Header.Set("User-Agent", "gitGost/1.0")
resp, err := client.Do(req)
if err != nil || resp.StatusCode != 200 {
if resp != nil {
resp.Body.Close()
}
continue
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
c.Data(200, "text/plain; charset=utf-8", body)
return
}
c.JSON(http.StatusNotFound, gin.H{"error": "wiki page not found"})
}
func GitHubWikiProxyHandler(c *gin.Context) {
owner := c.Param("owner")
repo := c.Param("repo")
page := c.Param("page")
if page == "" {
page = "Home"
}
// intentar con .md primero, luego sin extension
pageURLs := []string{
fmt.Sprintf("https://raw.githubusercontent.com/wiki/%s/%s/%s.md", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(page)),
fmt.Sprintf("https://raw.githubusercontent.com/wiki/%s/%s/%s", url.PathEscape(owner), url.PathEscape(repo), url.PathEscape(page)),
}
client := &http.Client{Timeout: 15 * time.Second}
for _, pageURL := range pageURLs {
req, err := http.NewRequest("GET", pageURL, nil)
if err != nil {
continue
}
req.Header.Set("Accept", "text/plain")
req.Header.Set("User-Agent", "gitGost/1.0")
resp, err := client.Do(req)
if err != nil || resp.StatusCode != 200 {
if resp != nil {
resp.Body.Close()
}
continue
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
c.Data(200, "text/plain; charset=utf-8", body)
return
}
c.JSON(http.StatusNotFound, gin.H{"error": "wiki page not found"})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/http/handlers.go` around lines 1747 - 1785, Update
GitHubWikiProxyHandler to URL-escape the owner, repo, and page path segments
before constructing pageURLs, ensuring characters such as #, ?, &, and spaces
remain part of the intended wiki path. Preserve the existing .md fallback and
request behavior.

Comment thread internal/http/handlers.go
Comment on lines +1966 to +2002
userToken := req.UserToken
if strings.TrimSpace(userToken) == "" {
userToken = generateUserToken()
}
hash := deriveHash(owner, repo, number, userToken)
reports := getReportCountWithWindow(c.Request.Context(), hash)
if reports > 5 {
c.JSON(http.StatusForbidden, gin.H{"error": "hash bloqueado por reportes"})
return
}
if reports > 2 {
if blocked := isFlaggedCooldown(hash); blocked {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "cooldown activo por reportes"})
return
}
}
currentKarma := getKarma(c.Request.Context(), hash)
karma := currentKarma + 1
if reports > 2 {
karma = 0
}
updateKarma(c.Request.Context(), hash, karma)
if reports > 2 {
markFlaggedAction(hash)
}
reportURL := fmt.Sprintf("%s://%s/v1/moderation/report?hash=%s", getScheme(c.Request), c.Request.Host, hash)

legend := fmt.Sprintf("\n\n---\ngoster-%s · karma (%d) · [report](%s)", hash, karma, reportURL)
bodyWithLegend := req.Body + legend

prov := providerFromPath(c.Request.URL.Path)
commentURL, err := prov.CreateAnonymousDiscussionComment(owner, repo, number, bodyWithLegend)
if err != nil {
utils.Log("Error creating discussion comment: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check ordering of karma/report mutation vs provider call in sibling handlers
rg -n -B5 -A15 'CreateAnonymousComment\(|CreateAnonymousPRComment\(' internal/http/handlers.go | rg -n 'updateKarma|markFlaggedAction|CreateAnonymous(PR)?Comment\('

Repository: livrasand/gitGost

Length of output: 342


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant handler blocks and the helper functions they call.
sed -n '1800,1865p' internal/http/handlers.go
printf '\n----\n'
sed -n '1885,1945p' internal/http/handlers.go
printf '\n----\n'
sed -n '1950,2015p' internal/http/handlers.go
printf '\n----\n'
rg -n 'func (updateKarma|markFlaggedAction|isFlaggedCooldown|getReportCountWithWindow|deriveHash|generateUserToken|providerFromPath)' internal/http/handlers.go

Repository: livrasand/gitGost

Length of output: 6388


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '2170,2265p' internal/http/handlers.go

Repository: livrasand/gitGost

Length of output: 2178


Move karma/cooldown updates after the provider call.

updateKarma and markFlaggedAction run before prov.CreateAnonymousDiscussionComment, so a provider error still persists the new karma and can start cooldown from a failed attempt. The same ordering is used in the other anonymous comment handlers, so fix the shared pattern in one pass.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/http/handlers.go` around lines 1966 - 2002, Move the karma and
cooldown mutations from before the provider call to after a successful
prov.CreateAnonymousDiscussionComment in the anonymous comment handler, keeping
the existing reports-based karma calculation and blocking checks unchanged.
Apply the same ordering change to the corresponding anonymous comment handlers
that use updateKarma and markFlaggedAction, so provider failures do not persist
state.

Comment thread internal/http/router.go
Comment on lines +323 to +325
api.GET("/gh-discussions/:owner/:repo", GitHubDiscussionsProxyHandler)
api.GET("/gh-discussion/:owner/:repo/:number", GitHubDiscussionDetailProxyHandler)
api.GET("/gh-wiki/:owner/:repo/:page", GitHubWikiProxyHandler)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

New public /api proxy routes have no rate limiting, unlike sibling routes in this file.

gh-discussions, gh-discussion, and gh-wiki (handlers in internal/http/handlers.go) are registered here with no middleware beyond the global securityHeaders/localhostCORS — no sizeLimitMiddleware, no per-IP limiter comparable to prCheckLimiter() (line 313) or adminLimiter() (line 336). The first two proxy to GitHub's GraphQL endpoint using the server's shared GITHUB_TOKEN, whose GraphQL point budget (5000/hr) is easy to exhaust from a single unauthenticated client hammering this route, degrading every other feature relying on the same token (anonymous issue/PR/discussion comments, badges). This runs counter to the PR's stated goal of addressing rate-limiting issues.

🛡️ Suggested direction
-		api.GET("/gh-discussions/:owner/:repo", GitHubDiscussionsProxyHandler)
-		api.GET("/gh-discussion/:owner/:repo/:number", GitHubDiscussionDetailProxyHandler)
-		api.GET("/gh-wiki/:owner/:repo/:page", GitHubWikiProxyHandler)
+		api.GET("/gh-discussions/:owner/:repo", ghProxyLimiter(), GitHubDiscussionsProxyHandler)
+		api.GET("/gh-discussion/:owner/:repo/:number", ghProxyLimiter(), GitHubDiscussionDetailProxyHandler)
+		api.GET("/gh-wiki/:owner/:repo/:page", ghProxyLimiter(), GitHubWikiProxyHandler)

(ghProxyLimiter being a new limiter analogous to prCheckLimiter, plus optional short-TTL caching in the handlers to further cut redundant upstream calls.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/http/router.go` around lines 323 - 325, Update the route
registrations for GitHubDiscussionsProxyHandler,
GitHubDiscussionDetailProxyHandler, and GitHubWikiProxyHandler to apply a
dedicated per-IP ghProxyLimiter, analogous to prCheckLimiter, before invoking
these public proxy handlers. Preserve the existing route paths and middleware
while ensuring unauthenticated clients cannot exhaust the shared GitHub token
budget; add short-TTL handler caching only if needed to reduce redundant
upstream requests.

Comment thread web/repo.html
Comment on lines +3583 to +3605
const res = await ghFetch(`https://api.github.com/repos/${rv.owner}/${rv.repo}/releases?per_page=1`);
if (!res.ok) { el.textContent = ''; if (section) section.style.display = 'none'; return; }
const link = res.headers.get('Link') || '';
const match = link.match(/&page=(\d+)>; rel="last"/);
const count = match ? parseInt(match[1]) : 0;
const releases = await res.json();
const latest = releases[0];
if (count > 0 && latest) {
el.textContent = count.toLocaleString();
if (latestEl) {
const tag = latest.tag_name || latest.name || '';
const name = latest.name || tag;
const ago = timeAgo(latest.published_at || latest.created_at);
latestEl.innerHTML = `<a href="#" onclick="showView('releases'); return false;" style="color:var(--fg);text-decoration:none;display:block;padding:0.3rem 0;">
<div style="font-size:0.82rem;font-weight:500;line-height:1.3;">${escHtml(name)} <span class="release-latest-badge">Latest</span></div>
${ago ? `<div style="font-size:0.7rem;color:var(--fg-muted);margin-top:0.15rem;">${ago}</div>` : ''}
</a>`;
}
if (section) section.style.display = '';
} else {
el.textContent = '';
if (section) section.style.display = 'none';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Single-release GitHub repos hide the Releases sidebar.

With per_page=1, GitHub omits the Link header when there is only one release, so count stays 0 and the count > 0 && latest guard hides the section even though a release exists. The GitLab branch already falls back to data.length; mirror that here.

🐛 Proposed fix
                     const link = res.headers.get('Link') || '';
                     const match = link.match(/&page=(\d+)>; rel="last"/);
-                    const count = match ? parseInt(match[1]) : 0;
                     const releases = await res.json();
+                    const count = match ? parseInt(match[1]) : releases.length;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const res = await ghFetch(`https://api.github.com/repos/${rv.owner}/${rv.repo}/releases?per_page=1`);
if (!res.ok) { el.textContent = ''; if (section) section.style.display = 'none'; return; }
const link = res.headers.get('Link') || '';
const match = link.match(/&page=(\d+)>; rel="last"/);
const count = match ? parseInt(match[1]) : 0;
const releases = await res.json();
const latest = releases[0];
if (count > 0 && latest) {
el.textContent = count.toLocaleString();
if (latestEl) {
const tag = latest.tag_name || latest.name || '';
const name = latest.name || tag;
const ago = timeAgo(latest.published_at || latest.created_at);
latestEl.innerHTML = `<a href="#" onclick="showView('releases'); return false;" style="color:var(--fg);text-decoration:none;display:block;padding:0.3rem 0;">
<div style="font-size:0.82rem;font-weight:500;line-height:1.3;">${escHtml(name)} <span class="release-latest-badge">Latest</span></div>
${ago ? `<div style="font-size:0.7rem;color:var(--fg-muted);margin-top:0.15rem;">${ago}</div>` : ''}
</a>`;
}
if (section) section.style.display = '';
} else {
el.textContent = '';
if (section) section.style.display = 'none';
}
const res = await ghFetch(`https://api.github.com/repos/${rv.owner}/${rv.repo}/releases?per_page=1`);
if (!res.ok) { el.textContent = ''; if (section) section.style.display = 'none'; return; }
const link = res.headers.get('Link') || '';
const match = link.match(/&page=(\d+)>; rel="last"/);
const releases = await res.json();
const count = match ? parseInt(match[1]) : releases.length;
const latest = releases[0];
if (count > 0 && latest) {
el.textContent = count.toLocaleString();
if (latestEl) {
const tag = latest.tag_name || latest.name || '';
const name = latest.name || tag;
const ago = timeAgo(latest.published_at || latest.created_at);
latestEl.innerHTML = `<a href="#" onclick="showView('releases'); return false;" style="color:var(--fg);text-decoration:none;display:block;padding:0.3rem 0;">
<div style="font-size:0.82rem;font-weight:500;line-height:1.3;">${escHtml(name)} <span class="release-latest-badge">Latest</span></div>
${ago ? `<div style="font-size:0.7rem;color:var(--fg-muted);margin-top:0.15rem;">${ago}</div>` : ''}
</a>`;
}
if (section) section.style.display = '';
} else {
el.textContent = '';
if (section) section.style.display = 'none';
}
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 3595-3598: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: latestEl.innerHTML = <a href="#" onclick="showView('releases'); return false;" style="color:var(--fg);text-decoration:none;display:block;padding:0.3rem 0;"> <div style="font-size:0.82rem;font-weight:500;line-height:1.3;">${escHtml(name)} <span class="release-latest-badge">Latest</span></div> ${ago ?

${ago}
: ''} </a>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/repo.html` around lines 3583 - 3605, Update the GitHub releases logic
around the count calculation and the count > 0 && latest guard to fall back to
releases.length when the Link header does not provide a page count. Ensure
repositories with one release display the sidebar and latest release while
preserving the existing Link-header count for multi-release repositories.

Comment thread web/repo.html
Comment on lines +4118 to +4139
function renderMd(md, baseUrl, isWiki) {
if (typeof marked === 'undefined') return `<pre>${escHtml(md)}</pre>`;
marked.setOptions({ gfm: true, breaks: false });
marked.setOptions({ gfm: true, breaks: false, headerIds: true, headerPrefix: '' });
const renderer = new marked.Renderer();
// marked v4: link token es un objeto {href, title, text}
renderer.link = function(token) {
const href = (token && token.href != null) ? token.href : (arguments[0] || '');
const title = (token && token.title != null) ? token.title : (arguments[1] || null);
const text = (token && token.text != null) ? token.text : (arguments[2] || '');
return `<a href="${href}" target="_blank" rel="noopener"${title ? ` title="${title}"` : ''}>${text}</a>`;
const t = title ? ` title="${escAttr(title)}"` : '';
if (typeof href === 'string' && href.startsWith('#')) {
const anchor = href.slice(1);
return anchor ? `<a href="#" data-anchor="${escAttr(anchor)}"${t}>${text}</a>` : `<a href="#"${t}>${text}</a>`;
}
if (isWiki && typeof href === 'string' && !/^[a-z][a-z0-9+.-]*:/i.test(href) && !/^\/\//.test(href)) {
const p = parseWikiHref(href);
if (p.slug || p.anchor) {
const target = p.slug + (p.anchor ? '#' + p.anchor : '');
return `<a href="#" data-wiki="${escAttr(target)}"${t}>${text}</a>`;
}
}
return `<a href="${escAttr(href)}" target="_blank" rel="noopener"${t}>${text}</a>`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

marked v4 does it sanitize raw HTML in markdown by default

💡 Result:

No, Marked does not sanitize raw HTML by default in any version, including v4 [1][2][3]. Marked is a markdown parser and does not perform security filtering on its output [1][2]. If you process untrusted markdown input that may contain raw HTML, you are responsible for sanitizing the resulting HTML to prevent Cross-Site Scripting (XSS) attacks [1][4]. The official documentation consistently recommends using a dedicated sanitization library such as DOMPurify, sanitize-html, or insane on the output generated by Marked [2][4]. For example, you can integrate sanitization using a post-process hook in Marked: import { marked } from 'marked'; import DOMPurify from 'dompurify'; marked.use({ hooks: { postprocess(html) { return DOMPurify.sanitize(html); } } }); const html = marked.parse(userMarkdown); Older versions of Marked included a 'sanitize' option, but this was removed in v8.0.0 (and was already deprecated long before that) [5][6]. Regardless of the version, Marked's architecture has historically prioritized performance and extensibility over built-in security sanitization [1][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and locate the renderer + sinks.
ast-grep outline web/repo.html --view expanded || true

printf '\n--- renderMd occurrences ---\n'
rg -n "renderMd\s*\(" web/repo.html || true

printf '\n--- innerHTML assignments mentioning renderMd ---\n'
rg -n "innerHTML\s*=.*renderMd|renderMd.*innerHTML" web/repo.html || true

printf '\n--- relevant surrounding lines around renderMd ---\n'
line=$(rg -n "function renderMd\(md, baseUrl, isWiki\)" web/repo.html | head -n1 | cut -d: -f1)
if [ -n "${line:-}" ]; then
  start=$((line-20))
  end=$((line+140))
  sed -n "${start},${end}p" web/repo.html | cat -n
fi

Repository: livrasand/gitGost

Length of output: 12435


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate all places that convert markdown to HTML and write to DOM.
rg -n "innerHTML\s*=|insertAdjacentHTML|outerHTML\s*=|renderMd\(" web/repo.html || true

printf '\n--- nearby source selection logic ---\n'
rg -n "discussion|release note|release notes|comment|body|markdown|md" web/repo.html | head -n 80 || true

Repository: livrasand/gitGost

Length of output: 15881


Sanitize markdown output before assigning it to innerHTML.

renderMd uses marked.parse() and the result is injected into multiple DOM sinks without sanitization. Since the content comes from untrusted issue/discussion/release/wiki bodies, raw HTML in markdown (for example, <img onerror=...>) can execute. Pass the rendered HTML through DOMPurify or a similar sanitizer at each sink.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/repo.html` around lines 4118 - 4139, Sanitize the HTML produced by marked
in renderMd before it reaches any innerHTML assignment, using the existing
DOMPurify integration or an equivalent sanitizer. Apply this at every renderMd
output sink for untrusted issue, discussion, release, and wiki content, while
preserving the existing link-rendering behavior.

Source: Linters/SAST tools

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