-
Notifications
You must be signed in to change notification settings - Fork 7.6k
feat(extensions): authenticate GitHub-hosted catalog and download requests with GITHUB_TOKEN/GH_TOKEN #2087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
147a0af
c4ef1d1
eb4f894
f32d059
2ccd213
8a918e6
9c0dc2e
4053922
1d63f2d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -93,9 +93,25 @@ See [scaffold/](scaffold/) for a scaffold you can copy to create your own preset | |||||
|
|
||||||
| ## Environment Variables | ||||||
|
|
||||||
| | Variable | Description | | ||||||
| |----------|-------------| | ||||||
| | `SPECKIT_PRESET_CATALOG_URL` | Override the catalog URL (replaces all defaults) | | ||||||
| | Variable | Description | Default | | ||||||
| |----------|-------------|---------| | ||||||
| | `SPECKIT_PRESET_CATALOG_URL` | Override the full catalog stack with a single URL (replaces all defaults) | Built-in default stack | | ||||||
| | `GH_TOKEN` / `GITHUB_TOKEN` | GitHub token for authenticated requests to GitHub-hosted URLs (`raw.githubusercontent.com`, `github.com`, `api.github.com`). Required when your catalog JSON or preset ZIPs are hosted in a private GitHub repository. | None | | ||||||
|
||||||
| | `GH_TOKEN` / `GITHUB_TOKEN` | GitHub token for authenticated requests to GitHub-hosted URLs (`raw.githubusercontent.com`, `github.com`, `api.github.com`). Required when your catalog JSON or preset ZIPs are hosted in a private GitHub repository. | None | | |
| | `GH_TOKEN` / `GITHUB_TOKEN` | GitHub token for authenticated requests to GitHub-hosted URLs (`raw.githubusercontent.com`, `github.com`, `api.github.com`, `codeload.github.com`). Required when your catalog JSON or preset ZIPs are hosted in a private GitHub repository. | None | |
Copilot
AI
Apr 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR title/description focus on ExtensionCatalog, but this change also introduces the same GitHub-token behavior for PresetCatalog and documents it here. Please update the PR title and/or description to reflect that presets are included as well, so reviewers and release notes capture the full scope.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||||||||||||||||||||||||||||
| """Shared GitHub-authenticated HTTP helpers. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Used by both ExtensionCatalog and PresetCatalog to attach | ||||||||||||||||||||||||||||||||
| GITHUB_TOKEN / GH_TOKEN credentials to requests targeting | ||||||||||||||||||||||||||||||||
| GitHub-hosted domains, while preventing token leakage to | ||||||||||||||||||||||||||||||||
| third-party hosts on redirects. | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||
| import urllib.request | ||||||||||||||||||||||||||||||||
| from urllib.parse import urlparse | ||||||||||||||||||||||||||||||||
| from typing import Dict | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # GitHub-owned hostnames that should receive the Authorization header. | ||||||||||||||||||||||||||||||||
| # Includes codeload.github.com because GitHub archive URL downloads | ||||||||||||||||||||||||||||||||
| # (e.g. /archive/refs/tags/<tag>.zip) redirect there and require auth | ||||||||||||||||||||||||||||||||
| # for private repositories. | ||||||||||||||||||||||||||||||||
| GITHUB_HOSTS = frozenset({ | ||||||||||||||||||||||||||||||||
| "raw.githubusercontent.com", | ||||||||||||||||||||||||||||||||
| "github.com", | ||||||||||||||||||||||||||||||||
| "api.github.com", | ||||||||||||||||||||||||||||||||
| "codeload.github.com", | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def build_github_request(url: str) -> urllib.request.Request: | ||||||||||||||||||||||||||||||||
| """Build a urllib Request, adding a GitHub auth header when available. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Reads GITHUB_TOKEN or GH_TOKEN from the environment and attaches an | ||||||||||||||||||||||||||||||||
| ``Authorization: token <value>`` header when the target hostname is one | ||||||||||||||||||||||||||||||||
| of the known GitHub-owned domains. Non-GitHub URLs are returned as plain | ||||||||||||||||||||||||||||||||
| requests so credentials are never leaked to third-party hosts. | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
| headers: Dict[str, str] = {} | ||||||||||||||||||||||||||||||||
| github_token = (os.environ.get("GITHUB_TOKEN") or "").strip() | ||||||||||||||||||||||||||||||||
| gh_token = (os.environ.get("GH_TOKEN") or "").strip() | ||||||||||||||||||||||||||||||||
| token = github_token or gh_token or None | ||||||||||||||||||||||||||||||||
| hostname = (urlparse(url).hostname or "").lower() | ||||||||||||||||||||||||||||||||
| if token and hostname in GITHUB_HOSTS: | ||||||||||||||||||||||||||||||||
| headers["Authorization"] = f"token {token}" | ||||||||||||||||||||||||||||||||
| return urllib.request.Request(url, headers=headers) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| class _StripAuthOnRedirect(urllib.request.HTTPRedirectHandler): | ||||||||||||||||||||||||||||||||
| """Redirect handler that drops the Authorization header when leaving GitHub. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Prevents token leakage to CDNs or other third-party hosts that GitHub | ||||||||||||||||||||||||||||||||
| may redirect to (e.g. S3 for release asset downloads, objects.githubusercontent.com). | ||||||||||||||||||||||||||||||||
| Auth is preserved as long as the redirect target remains within GITHUB_HOSTS. | ||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| def redirect_request(self, req, fp, code, msg, headers, newurl): | ||||||||||||||||||||||||||||||||
| new_req = super().redirect_request(req, fp, code, msg, headers, newurl) | ||||||||||||||||||||||||||||||||
| if new_req is not None: | ||||||||||||||||||||||||||||||||
| hostname = (urlparse(newurl).hostname or "").lower() | ||||||||||||||||||||||||||||||||
| if hostname not in GITHUB_HOSTS: | ||||||||||||||||||||||||||||||||
| new_req.headers.pop("Authorization", None) | ||||||||||||||||||||||||||||||||
|
Comment on lines
+53
to
+57
|
||||||||||||||||||||||||||||||||
| new_req = super().redirect_request(req, fp, code, msg, headers, newurl) | |
| if new_req is not None: | |
| hostname = (urlparse(newurl).hostname or "").lower() | |
| if hostname not in GITHUB_HOSTS: | |
| new_req.headers.pop("Authorization", None) | |
| original_auth = req.get_header("Authorization") | |
| new_req = super().redirect_request(req, fp, code, msg, headers, newurl) | |
| if new_req is not None: | |
| hostname = (urlparse(newurl).hostname or "").lower() | |
| if hostname in GITHUB_HOSTS: | |
| if original_auth: | |
| new_req.add_unredirected_header("Authorization", original_auth) | |
| else: | |
| new_req.headers.pop("Authorization", None) | |
| new_req.unredirected_hdrs.pop("Authorization", None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docs enumerate GitHub hosts as
raw.githubusercontent.com,github.com, andapi.github.com, but the implementation also treatscodeload.github.comas GitHub-owned (and tests rely on it). Please either includecodeload.github.comin this list or adjust the wording so the parenthetical isn’t interpreted as exhaustive.