Skip to content

feat(token): use github proxy instead of retrieving raw tokens#56

Open
risu729 wants to merge 11 commits into
jdx:mainfrom
risu729:github-proxy
Open

feat(token): use github proxy instead of retrieving raw tokens#56
risu729 wants to merge 11 commits into
jdx:mainfrom
risu729:github-proxy

Conversation

@risu729
Copy link
Copy Markdown
Contributor

@risu729 risu729 commented Jan 7, 2026

This PR removes the token manager (currently kept for compatibility) and implements a GitHub proxy at https://mise-versions.jdx.dev/gh/ that automatically forwards requests with a rotated GitHub API token.
This will slightly improve the security by keeping the tokens in the server, and also make the tokens easier to use, as we no longer need to implement retry logic on the consumer side.

Requires jdx/mise#7593. Will replace jdx/mise#7397.

Copilot AI review requested due to automatic review settings January 7, 2026 20:49
@risu729 risu729 marked this pull request as draft January 7, 2026 20:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the GitHub token management system by replacing direct token retrieval with a centralized GitHub proxy approach. Instead of clients fetching tokens from a token manager API and handling rate limits themselves, they now route all GitHub API requests through a proxy endpoint that manages tokens and rate limiting server-side.

Key Changes:

  • Introduces a new GitHub proxy endpoint (web/src/pages/gh/[...path].ts) that handles token rotation and rate limiting transparently
  • Updates all scripts and workflows to use GITHUB_PROXY_URL and API_SECRET instead of TOKEN_MANAGER_URL and TOKEN_MANAGER_SECRET
  • Removes client-side token management code including scripts/github-token.js

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
web/src/pages/gh/[...path].ts New GitHub proxy endpoint that manages token rotation, rate limiting, and retries server-side
scripts/update.sh Refactored to use proxy via MISE_URL_REPLACEMENTS instead of managing tokens directly; removed token manager functions
scripts/migrate.js Updated environment variable names and removed trailing whitespace
scripts/github-token.js Deleted - token management logic moved to server-side proxy
scripts/fetch-metadata.js Updated to use GitHub proxy for API calls; removed client-side token rotation logic
scripts/backfill-created-at.js Updated to use GitHub proxy via MISE_URL_REPLACEMENTS; removed token manager integration
CLAUDE.md Updated documentation to reflect proxy-based token management
.github/workflows/update.yml Updated to use GITHUB_PROXY_URL and API_SECRET; fixed boolean default value
.github/workflows/tool-analysis.yml Consolidated GITHUB_TOKEN environment variable; removed trailing whitespace
.github/workflows/python.yml Consolidated GITHUB_TOKEN environment variable
.github/workflows/metadata.yml Removed TOKEN_MANAGER_* variables and added GITHUB_PROXY_URL
.github/workflows/backfill.yml Removed TOKEN_MANAGER_* variables and added GITHUB_PROXY_URL with API_SECRET
.github/workflows/aqua.yml Consolidated GITHUB_TOKEN environment variable

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +62 to +64
// Forward request
const headers = new Headers(request.headers);
headers.set("Authorization", `Bearer ${token.token}`);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

The proxy forwards all request headers from the client to GitHub API, which could potentially leak sensitive information or cause unexpected behavior. Consider explicitly allowlisting headers to forward (e.g., Accept, User-Agent, If-None-Match) instead of forwarding all headers, particularly removing the original Authorization header before setting the new one with the token.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We basically use this proxy from GitHub Actions, so this should be fine. We need to remove some headers if GitHub refuses access with them (e.g. headers by Cloudflare).

Comment thread web/src/pages/gh/[...path].ts
Comment thread web/src/pages/gh/[...path].ts Outdated
Comment thread scripts/backfill-created-at.js Outdated
Comment thread web/src/pages/gh/[...path].ts
Comment thread .github/workflows/update.yml
Comment thread scripts/update.sh
Comment thread scripts/update.sh
Comment thread scripts/update.sh
@jdx
Copy link
Copy Markdown
Owner

jdx commented Jan 7, 2026

Great idea!

@risu729 risu729 marked this pull request as ready for review January 8, 2026 16:20
@risu729 risu729 marked this pull request as draft February 11, 2026 15:46
Deactivate tokens immediately when GitHub returns 401 so invalid or revoked tokens are not retried across requests.

Made-with: Cursor
Bring back registry-based cleanup so stale docs files are removed when tools no longer exist in the current mise registry.

Made-with: Cursor
@risu729
Copy link
Copy Markdown
Contributor Author

risu729 commented Feb 28, 2026

/gemini review

@risu729 risu729 marked this pull request as ready for review February 28, 2026 18:40
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the GitHub token handling by introducing a server-side proxy. This removes the client-side token manager, simplifying the scripts in scripts/ and improving security by not exposing raw tokens to the clients. The changes are well-executed, removing the old token management logic and adapting the scripts to use the new proxy via environment variables. I've found a minor issue in scripts/backfill-created-at.js where a function can be simplified.

Comment thread scripts/backfill-created-at.js Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Feb 28, 2026

Greptile Summary

This PR replaces the client-side token manager with a server-side GitHub API proxy at https://mise-versions.jdx.dev/gh/. Instead of scripts retrieving raw GitHub tokens and managing rate-limit rotation themselves, all GitHub API requests are now routed through the new proxy endpoint (web/src/pages/gh/[...path].ts), which authenticates callers with API_SECRET, selects a token from the D1 pool via round-robin/LRU, and automatically retries with a fresh token on rate-limit (403/429) or revocation (401) responses.

Key changes:

  • New Astro/Cloudflare Workers endpoint web/src/pages/gh/[...path].ts implements the proxy with up to 3 retry attempts and token deactivation on 401.
  • src/database.ts adds a deactivateToken method used when GitHub returns 401.
  • scripts/github-token.js deleted — raw token retrieval is no longer needed client-side.
  • scripts/update.sh, scripts/fetch-metadata.js, and scripts/backfill-created-at.js updated to use MISE_URL_REPLACEMENTS to redirect GitHub API calls through the proxy, passing API_SECRET as the bearer token.
  • GitHub Actions workflows updated to remove TOKEN_MANAGER_* vars and scope proxy env vars to the appropriate steps.
  • Notable: The proxy's 403 rate-limit condition requires both x-ratelimit-remaining === "0" AND x-ratelimit-reset to be present; if x-ratelimit-reset is absent the token is not marked and the exhausted token stays in the pool without a retry. Additionally, all incoming client request headers (including internal Cloudflare headers) are forwarded verbatim to GitHub before Authorization is overridden.

Confidence Score: 4/5

  • This PR is safe to merge; the proxy architecture is sound and the edge cases flagged are minor.
  • The core proxy logic is correct: authentication is enforced, tokens are rotated on rate-limit/401, and the overall design improves security by keeping raw tokens server-side. The main concern is the 403 edge case where a rate-limited token can stay in the pool if GitHub omits x-ratelimit-reset (unlikely in practice), and forwarding all client headers to GitHub (harmless but untidy). These are P2 concerns that don't break the primary path.
  • web/src/pages/gh/[...path].ts — review the 403 rate-limit condition and header forwarding logic.

Important Files Changed

Filename Overview
web/src/pages/gh/[...path].ts New GitHub proxy endpoint with token rotation — core of the PR. Logic issues around 403 handling edge cases, header forwarding, and response header exposure noted.
src/database.ts Adds deactivateToken method for 401 responses; clean, minimal change that integrates well with existing markTokenRateLimited pattern.
scripts/update.sh Removes token manager calls and replaces with MISE_URL_REPLACEMENTS + MISE_GITHUB_TOKEN proxy config; also removes early-exit guard on missing token management setup, which is now expected to be optional.
scripts/fetch-metadata.js Removes token manager state management; GitHub calls now routed through proxy. The existing !response.ok guard properly catches proxy error responses (502/503).
scripts/backfill-created-at.js Simplified by removing per-retry token refresh loop; proxy handles rotation server-side. Falls back gracefully to GITHUB_TOKEN when proxy is not configured.
scripts/github-token.js Deleted entirely; replaced by the server-side proxy. Removes client-side token exposure, which is the primary security motivation of this PR.
.github/workflows/update.yml Replaces GITHUB_API_TOKEN/GH_TOKEN/TOKEN_MANAGER_* env vars with GITHUB_TOKEN at workflow level and GITHUB_PROXY_URL/API_SECRET scoped to the update step.
.github/workflows/backfill.yml Moves proxy-specific env vars to the job step level, keeping them appropriately scoped.

Sequence Diagram

sequenceDiagram
    participant mise as mise / scripts
    participant proxy as GitHub Proxy<br/>(mise-versions.jdx.dev/gh/...)
    participant db as D1 Token DB
    participant gh as api.github.com

    mise->>proxy: GET /gh/{path}<br/>Authorization: Bearer <API_SECRET>
    proxy->>proxy: requireApiAuth(API_SECRET)
    proxy->>db: deactivateExpiredTokens() [async]
    proxy->>db: getNextToken() [round-robin, LRU]
    db-->>proxy: token {id, token}

    proxy->>gh: GET /{path}<br/>Authorization: Bearer <github_token>
    gh-->>proxy: response + x-ratelimit-* headers

    alt 403/429 rate limited
        proxy->>db: markTokenRateLimited(token.id, resetAt)
        proxy->>db: getNextToken() [retry, up to 3x]
        db-->>proxy: next token
        proxy->>gh: retry with new token
        gh-->>proxy: 200 OK
    else 401 unauthorized
        proxy->>db: deactivateToken(token.id)
        proxy->>db: getNextToken() [retry]
        db-->>proxy: next token
        proxy->>gh: retry with new token
        gh-->>proxy: 200 OK
    else success
        proxy-->>mise: forward response body + headers
    end
Loading

Last reviewed commit: "Merge branch 'main' ..."

risu729 added 2 commits March 1, 2026 06:05
Drop the unused retries parameter from timestamp fetch helper and update the call site to match current proxy-driven retry behavior.

Made-with: Cursor
Comment on lines +59 to +63
if (
(rateLimitRemaining === "0" && rateLimitReset) ||
response.status === 429
) {
isRateLimited = true;
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 Rate-limited 403 without x-ratelimit-reset leaves bad token in pool

When GitHub returns a 403 with x-ratelimit-remaining: 0 but does not include an x-ratelimit-reset header, neither branch of the inner if/else if triggers:

  • (rateLimitRemaining === "0" && rateLimitReset)false (because rateLimitReset is null)
  • response.status === 429false
  • response.status === 401false

isRateLimited stays false, shouldRetry evaluates to false, and the 403 is returned directly to the caller — without the token being marked or the loop retrying. On the very next request, getNextToken() could select the same exhausted token again, producing another immediate 403.

GitHub almost always includes x-ratelimit-reset, but to be safe the condition could default to a 1-hour mark when the header is absent:

Suggested change
if (
(rateLimitRemaining === "0" && rateLimitReset) ||
response.status === 429
) {
isRateLimited = true;
if (
(rateLimitRemaining === "0") ||
response.status === 429
) {

With this change the existing fallback (let resetDate = new Date(Date.now() + 60 * 60 * 1000)) is used when rateLimitReset is absent, which is already handled a few lines below.

Comment on lines +39 to +41
// Forward request
const headers = new Headers(request.headers);
headers.set("Authorization", `Bearer ${token.token}`);
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 All incoming client headers forwarded to GitHub API

new Headers(request.headers) copies every incoming header verbatim — including Authorization: Bearer <api_secret> (which is correctly overridden one line later), Cookie, and Cloudflare internal headers like CF-Connecting-IP and CF-Ray. While the Authorization override prevents the API secret from leaking, the other headers are forwarded unnecessarily.

Consider building a fresh, minimal headers object instead of copying the full client request:

Suggested change
// Forward request
const headers = new Headers(request.headers);
headers.set("Authorization", `Bearer ${token.token}`);
const headers = new Headers({
Authorization: `Bearer ${token.token}`,
Accept: request.headers.get("Accept") ?? "application/vnd.github.v3+json",
"User-Agent": request.headers.get("User-Agent") ?? "mise-versions",
});

This ensures only intentional, non-sensitive headers reach the GitHub API upstream.

Comment on lines +89 to +94
if (!shouldRetry) {
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
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 GitHub internal headers (e.g. X-OAuth-Scopes) forwarded to callers

Passing headers: response.headers directly to the client includes GitHub API internal response headers such as X-OAuth-Scopes, X-Authenticated-OAuth-Client-Id, and X-GitHub-Request-Id. These reveal the scopes and identity of the token pool being used.

Although access is already gated by API_SECRET, stripping (or explicitly allowlisting) these headers before returning is a defensive-in-depth measure:

Suggested change
if (!shouldRetry) {
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
const filteredHeaders = new Headers(response.headers);
for (const h of ["x-oauth-scopes", "x-authenticated-oauth-client-id"]) {
filteredHeaders.delete(h);
}
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: filteredHeaders,
});

@risu729
Copy link
Copy Markdown
Contributor Author

risu729 commented Apr 24, 2026

@jdx Are you planning to merge this? If so, I'm happy to resolve conflicts/reviews.

@jdx
Copy link
Copy Markdown
Owner

jdx commented Apr 24, 2026

hold off for now, I'm not sure if I want to

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.

3 participants