Skip to content

Dev#1

Merged
Noisemaker111 merged 14 commits into
mainfrom
dev
Feb 13, 2026
Merged

Dev#1
Noisemaker111 merged 14 commits into
mainfrom
dev

Conversation

@Noisemaker111

@Noisemaker111 Noisemaker111 commented Feb 13, 2026

Copy link
Copy Markdown
Owner

@vercel

vercel Bot commented Feb 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
opencode-mono-web Ready Ready Preview, Comment Feb 13, 2026 6:22am

@devin-ai-integration devin-ai-integration 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

@devin-ai-integration devin-ai-integration 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.

Devin Review found 2 new potential issues.

View 8 additional findings in Devin Review.

Open in Devin Review

Comment on lines +127 to +134
export const refreshRepositoryStars = action({
args: {
owner: v.string(),
repo: v.string(),
},
handler: async (ctx, args) => {
const now = Date.now();
const current = await ctx.runQuery(api.github.getRepositoryStars, args);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Public action leaks server GITHUB_TOKEN to query arbitrary repositories

The refreshRepositoryStars action is a public Convex action (not internalAction) that accepts arbitrary owner and repo strings from any unauthenticated client. The server then uses its GITHUB_TOKEN to make authenticated requests to any GitHub repository the caller specifies.

Root Cause and Impact

At packages/backend/convex/github.ts:153-159, the server constructs a GitHub API URL from user-supplied args.owner and args.repo and attaches the server's GITHUB_TOKEN as a Bearer token:

const endpoint = `https://api.github.com/repos/${encodeURIComponent(args.owner)}/${encodeURIComponent(args.repo)}`;
const githubToken = process.env.GITHUB_TOKEN;
// ...
headers.Authorization = `Bearer ${githubToken.trim()}`;

The star count result is then stored in the database via setRepositoryStars and is readable by any client through the public getRepositoryStars query (packages/backend/convex/github.ts:35-56).

Impact:

  1. Information disclosure: An attacker can retrieve star counts of private repositories by calling refreshRepositoryStars({owner: "target-org", repo: "private-repo"}) then reading the result via getRepositoryStars. The star count of a private repo is not public information.
  2. Rate limit exhaustion: The cooldown is per-slug, so an attacker can use many different owner/repo combinations to bypass it entirely and exhaust the server's GitHub API rate limit.
  3. Database pollution: Arbitrary entries are created in githubRepositoryStats for any owner/repo combination.

The action should validate that the requested repository matches the expected RELEASE_REPOSITORY constant or a server-side whitelist.

Prompt for agents
In packages/backend/convex/github.ts, the refreshRepositoryStars action should validate that the requested owner/repo matches an expected allowlist (e.g. the RELEASE_REPOSITORY). Add a server-side constant or environment variable for the allowed repository slug, and reject requests that don't match. For example, add a check at the top of the handler like: const allowedSlug = process.env.ALLOWED_REPOSITORY_SLUG || 'Noisemaker111/opencode-mono'; const requestedSlug = `${args.owner}/${args.repo}`; if (requestedSlug !== allowedSlug) { return { refreshed: false, source: 'not-allowed' }; }. The same validation should be applied to getRepositoryStars if private repo star counts should not be queryable.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +755 to +758
if (stars >= 1_000) {
const thousands = stars / 1_000;
const value = thousands >= 10 ? Math.round(thousands).toString() : thousands.toFixed(1);
return `${value}k`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 formatStarCount produces "1000k" instead of "1M" due to rounding overflow

When stars is between 999,500 and 999,999, formatStarCount returns "1000k" instead of the expected "1M".

Detailed Explanation

At apps/web/src/routes/index.tsx:755-758, the code divides by 1000 and rounds:

if (stars >= 1_000) {
    const thousands = stars / 1_000;
    const value = thousands >= 10 ? Math.round(thousands).toString() : thousands.toFixed(1);
    return `${value}k`;
}

For stars = 999_500: thousands = 999.5, thousands >= 10 is true, Math.round(999.5) = 1000, so the function returns "1000k". The same class of issue exists at the millions boundary (e.g., stars = 9_999_500 would produce "10.0M" via toFixed(1) instead of "10M").

Expected: Values that round up past a boundary should be formatted with the next unit (e.g. "1M" not "1000k").

Impact: Cosmetic — the star badge in the menu bar would display an awkward label like "1000k Stars" for repos with ~999.5k stars.

Suggested change
if (stars >= 1_000) {
const thousands = stars / 1_000;
const value = thousands >= 10 ? Math.round(thousands).toString() : thousands.toFixed(1);
return `${value}k`;
if (stars >= 1_000) {
const thousands = stars / 1_000;
const value = thousands >= 10 ? Math.round(thousands).toString() : thousands.toFixed(1);
if (value === "1000") {
return "1M";
}
return `${value}k`;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

View 11 additional findings in Devin Review.

Open in Devin Review

Comment on lines +274 to +286
const windowsUrl = extractUpdaterUrl(
platformEntries,
(key, url) =>
key.includes("windows") || url.toLowerCase().includes(".msi") || url.toLowerCase().includes(".exe") || url.toLowerCase().includes("windows"),
);
const linuxUrl = extractUpdaterUrl(
platformEntries,
(key, url) =>
key.includes("linux") ||
url.toLowerCase().includes(".appimage") ||
url.toLowerCase().includes(".deb") ||
url.toLowerCase().includes(".rpm") ||
url.toLowerCase().includes("linux"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 windowsUrl and linuxUrl updater fallback matchers lack platform-key scoping, unlike macOS matchers

The extractUpdaterFallbackUrls function has an inconsistency in how it matches platform entries. The macOS matchers (lines 262-272) correctly use && to require both a platform key match AND an architecture/URL match. However, the windowsUrl matcher (lines 274-278) and the linuxUrl matcher (lines 279-286) use only top-level ||, meaning any entry from any platform could match if the URL happens to contain .msi, .exe, windows, .appimage, .deb, .rpm, or linux.

Root Cause

macOS matchers correctly scope:

(key.includes("darwin") || key.includes("mac")) &&
(key.includes("x86_64") || ...)

But Windows uses:

key.includes("windows") || url.toLowerCase().includes(".msi") || url.toLowerCase().includes(".exe") || url.toLowerCase().includes("windows")

Because the || is at the top level, the platform key requirement is optional. If the RELEASE_REPOSITORY name contains windows or linux, the resulting download URLs in the Tauri updater manifest would ALL contain that substring in their path, causing a darwin-aarch64 entry to be incorrectly selected as windowsUrl or linuxUrl. For example, a repo like user/my-windows-tool would make url.toLowerCase().includes("windows") match the macOS entry first, serving a macOS .app.tar.gz as the Windows download link.

Impact: Users on certain repo configurations could be offered wrong-platform download links when the updater manifest fallback is triggered.

Suggested change
const windowsUrl = extractUpdaterUrl(
platformEntries,
(key, url) =>
key.includes("windows") || url.toLowerCase().includes(".msi") || url.toLowerCase().includes(".exe") || url.toLowerCase().includes("windows"),
);
const linuxUrl = extractUpdaterUrl(
platformEntries,
(key, url) =>
key.includes("linux") ||
url.toLowerCase().includes(".appimage") ||
url.toLowerCase().includes(".deb") ||
url.toLowerCase().includes(".rpm") ||
url.toLowerCase().includes("linux"),
const windowsUrl = extractUpdaterUrl(
platformEntries,
(key, url) =>
key.includes("windows") &&
(key.includes("x86_64") || key.includes("x64") || url.toLowerCase().includes(".msi") || url.toLowerCase().includes(".exe")),
);
const linuxUrl = extractUpdaterUrl(
platformEntries,
(key, url) =>
key.includes("linux") &&
(url.toLowerCase().includes(".appimage") ||
url.toLowerCase().includes(".deb") ||
url.toLowerCase().includes(".rpm") ||
url.toLowerCase().includes("linux")),
);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

View 14 additional findings in Devin Review.

Open in Devin Review

const RELEASES_API_BASE_URL = `https://api.github.com/repos/${RELEASE_REPOSITORY}`;
const RELEASES_API_URL = `${RELEASES_API_BASE_URL}/releases/latest`;
const RELEASES_LIST_API_URL = `${RELEASES_API_BASE_URL}/releases?per_page=20`;
const UPDATER_MANIFEST_URL = `https://github.com/${RELEASE_REPOSITORY}/releases/latest/download/latest.json`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Updater manifest fallback URL always resolves to the latest stable release, giving wrong download links on the dev channel

The UPDATER_MANIFEST_URL at apps/web/src/routes/index.tsx:34 is constructed as https://github.com/${RELEASE_REPOSITORY}/releases/latest/download/latest.json. GitHub's /releases/latest endpoint always redirects to the latest non-prerelease release.

Root Cause and Impact

When RELEASE_CHANNEL === "dev" and the primary GitHub Releases API call fails (network error, rate limit, etc.), the code falls back to applyUpdaterManifestFallback() at lines 1001, 1016, and 1022. This function fetches UPDATER_MANIFEST_URL, which resolves to the latest stable release's latest.json, not the latest dev/prerelease release.

As a result, dev-channel users who hit the fallback path see download links pointing to stable release assets (e.g., stable .dmg and .exe URLs) instead of the expected prerelease/dev assets. The primary path correctly uses RELEASES_LIST_API_URL with pickReleaseForChannel to select a prerelease, but the fallback path has no equivalent channel awareness.

Impact: Dev-channel visitors may be directed to download stable binaries instead of dev binaries when the GitHub API is unavailable.

Prompt for agents
In apps/web/src/routes/index.tsx, the UPDATER_MANIFEST_URL on line 34 always uses /releases/latest/ which resolves to the latest non-prerelease release. For the dev channel, this gives the wrong assets. Consider either: (1) skipping the updater manifest fallback entirely when RELEASE_CHANNEL === 'dev' (since there is no reliable static URL for the latest prerelease's latest.json), or (2) constructing the updater manifest URL dynamically based on the dev release tag if available. The simplest fix is to guard the applyUpdaterManifestFallback calls (around lines 1001, 1016, 1022) so they only execute when RELEASE_CHANNEL === 'stable'.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@Noisemaker111 Noisemaker111 merged commit e781989 into main Feb 13, 2026
5 checks passed
@Noisemaker111 Noisemaker111 deleted the dev branch February 13, 2026 06:23

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 new potential issue.

View 16 additional findings in Devin Review.

Open in Devin Review

Comment on lines +1090 to +1097
return false;
}

if (!isActive) {
return false;
}

applyDownloadUi(buildDownloadOptions([], fallbackUrls), platform, architecture, null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Updater manifest URLs served as user-facing download links point to wrong file format

When the GitHub Releases API is unavailable, applyUpdaterManifestFallback fetches the Tauri updater manifest (latest.json) and uses its platform URLs as fallback download links. However, Tauri updater manifest URLs point to updater bundles (.app.tar.gz for macOS, .nsis.zip for Windows), not the user-installable artifacts (.dmg, .exe/.msi).

Root Cause and Impact

At apps/web/src/routes/index.tsx:1097, the fallback path calls:

applyDownloadUi(buildDownloadOptions([], fallbackUrls), platform, architecture, null);

with an empty assets array and fallbackUrls extracted from the updater manifest. Since primary assets is empty, buildDownloadOptions falls through to the fallback URLs at lines 377-405.

These fallback URLs come from extractUpdaterFallbackUrls (line 273-333) which reads platforms.*.url from the Tauri updater JSON. With "createUpdaterArtifacts": true in tauri.conf.json:63, these URLs point to compressed updater bundles:

  • macOS: OpenUsage.app.tar.gz (not .dmg)
  • Windows: OpenUsage_x64-setup.nsis.zip (not .exe or .msi)

Meanwhile, the download cards still display subtitles like "arm64 dmg", "x64 dmg", "x64 installer" — which are hardcoded in buildDownloadOptions at lines 413, 423, 432. Users would download a .tar.gz or .nsis.zip expecting a .dmg or .exe installer. On Windows especially, .nsis.zip cannot be installed directly without manual extraction.

Impact: When the GitHub releases API is rate-limited or unavailable, users are presented with misleading download links that deliver updater bundles instead of installer files, with incorrect file format labels.

Prompt for agents
The applyUpdaterManifestFallback function at apps/web/src/routes/index.tsx:1075-1101 uses Tauri updater manifest URLs as user-facing download links. These URLs point to updater bundles (.app.tar.gz, .nsis.zip) rather than installer files (.dmg, .exe). Two approaches to fix:

1. (Preferred) Transform updater manifest URLs to their corresponding installer asset URLs. Since GitHub release assets follow predictable naming, derive the .dmg/.exe URLs from the updater bundle URLs by stripping the updater suffixes (e.g., replace .app.tar.gz with .dmg, replace _x64-setup.nsis.zip with _x64-setup.exe).

2. (Alternative) If serving updater bundles is acceptable as a last resort, update the subtitle text in buildDownloadOptions to reflect the actual file format when fallback URLs are used (e.g., change subtitle from "arm64 dmg" to "arm64 tar.gz" when the URL ends in .tar.gz).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant