feat: update dependencies and improve request handling

This commit is contained in:
xixu-me committed 2026-03-09 17:37:27 +08:00
1 parent 3957f78d9d
commit 929337c494
16 files changed
+185 -563

No files matched your search

+6 -1
View File
@@ -8,7 +8,8 @@
"name": "xget",
"version": "1.0.0",
"dependencies": {
"express": "^5.2.1"
"express": "^5.2.1",
"xget": "file:"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.12.18",
@@ -6401,6 +6402,10 @@
}
}
},
"node_modules/xget": {
"resolved": "",
"link": true
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+2 -1
View File
@@ -1,6 +1,7 @@
{
"dependencies": {
"express": "^5.2.1"
"express": "^5.2.1",
"xget": "file:"
},
"devDependencies": {
"@commitlint/cli": "^20.1.0",
+4 -15
View File
@@ -132,27 +132,16 @@ import { PLATFORMS } from './platforms.js';
* // ['https://example.com', 'https://app.example.com']
*/
export function createConfig(env = {}) {
const allowedMethods =
typeof env.ALLOWED_METHODS === 'string'
? env.ALLOWED_METHODS.split(',')
.map(method => method.trim())
.filter(Boolean)
: ['GET', 'HEAD'];
const allowedOrigins =
typeof env.ALLOWED_ORIGINS === 'string'
? env.ALLOWED_ORIGINS.split(',')
.map(origin => origin.trim())
.filter(Boolean)
: ['*'];
return {
TIMEOUT_SECONDS: parseInt(String(env.TIMEOUT_SECONDS), 10) || 30,
MAX_RETRIES: parseInt(String(env.MAX_RETRIES), 10) || 3,
RETRY_DELAY_MS: parseInt(String(env.RETRY_DELAY_MS), 10) || 1000,
CACHE_DURATION: parseInt(String(env.CACHE_DURATION), 10) || 1800, // 30 minutes
SECURITY: {
ALLOWED_METHODS: allowedMethods.length ? allowedMethods : ['GET', 'HEAD'],
ALLOWED_ORIGINS: allowedOrigins.length ? allowedOrigins : ['*'],
ALLOWED_METHODS:
typeof env.ALLOWED_METHODS === 'string' ? env.ALLOWED_METHODS.split(',') : ['GET', 'HEAD'],
ALLOWED_ORIGINS:
typeof env.ALLOWED_ORIGINS === 'string' ? env.ALLOWED_ORIGINS.split(',') : ['*'],
MAX_PATH_LENGTH: parseInt(String(env.MAX_PATH_LENGTH), 10) || 2048
},
PLATFORMS
+13 -56
View File
@@ -21,8 +21,8 @@ import {
} from './protocols/docker.js';
import { configureGitHeaders, isGitLFSRequest, isGitRequest } from './protocols/git.js';
import { PerformanceMonitor, addPerformanceHeaders } from './utils/performance.js';
import { addCorsHeaders, addSecurityHeaders, createErrorResponse } from './utils/security.js';
import { getAllowedMethods, isDockerRequest, validateRequest } from './utils/validation.js';
import { addSecurityHeaders, createErrorResponse } from './utils/security.js';
import { isDockerRequest, validateRequest } from './utils/validation.js';
/**
* Main request handler with comprehensive caching, retry logic, and security measures.
@@ -40,36 +40,9 @@ async function handleRequest(request, env, ctx) {
const config = env ? createConfig(env) : CONFIG;
const url = new URL(request.url);
const isDocker = isDockerRequest(request, url);
const isCorsPreflight =
request.method === 'OPTIONS' &&
request.headers.has('Origin') &&
request.headers.has('Access-Control-Request-Method');
if (isCorsPreflight) {
const requestedMethod = request.headers.get('Access-Control-Request-Method') || '';
const allowedMethods = getAllowedMethods(
new Request(request.url, { method: requestedMethod || 'GET' }),
url,
config
);
if (!allowedMethods.includes(requestedMethod)) {
response = createErrorResponse('Method not allowed', 405);
} else {
const headers = addCorsHeaders(new Headers(), request, config);
if (!headers.has('Access-Control-Allow-Origin')) {
response = createErrorResponse('Origin not allowed', 403);
} else {
headers.set('Access-Control-Allow-Methods', allowedMethods.join(', '));
headers.set('Access-Control-Max-Age', '86400');
addSecurityHeaders(headers);
response = new Response(null, { status: 204, headers });
}
}
}
// Handle Docker API version check
else if (isDocker && (url.pathname === '/v2/' || url.pathname === '/v2')) {
if (isDocker && (url.pathname === '/v2/' || url.pathname === '/v2')) {
const headers = new Headers({
'Docker-Distribution-Api-Version': 'registry/2.0',
'Content-Type': 'application/json'
@@ -110,10 +83,7 @@ async function handleRequest(request, env, ctx) {
if (!response) {
// Handle Docker authentication explicitly
if (
isDocker &&
(url.pathname === '/v2/auth' || /^\/cr\/[^/]+\/v2\/auth\/?$/.test(url.pathname))
) {
if (isDocker && url.pathname === '/v2/auth') {
response = await handleDockerAuth(request, url, config);
} else {
// Platform detection using transform patterns
@@ -251,9 +221,7 @@ async function handleRequest(request, env, ctx) {
}
// Configure protocol-specific headers using modular helpers
if (isGit || isGitLFS) {
configureGitHeaders(requestHeaders, request, url, isGitLFS);
}
configureGitHeaders(requestHeaders, request, url, isGitLFS);
if (isAI) {
configureAIHeaders(requestHeaders, request);
@@ -269,6 +237,11 @@ async function handleRequest(request, env, ctx) {
http3: true,
cacheTtl: config.CACHE_DURATION,
cacheEverything: true,
minify: {
javascript: true,
css: true,
html: true
},
preconnect: true
}
});
@@ -276,10 +249,7 @@ async function handleRequest(request, env, ctx) {
requestHeaders.set('Accept-Encoding', 'gzip, deflate, br');
requestHeaders.set('Connection', 'keep-alive');
requestHeaders.set('User-Agent', 'Wget/1.21.3');
const origin = request.headers.get('Origin');
if (origin) {
requestHeaders.set('Origin', origin);
}
requestHeaders.set('Origin', request.headers.get('Origin') || '*');
if (authorization) {
requestHeaders.set('Authorization', authorization);
@@ -676,22 +646,9 @@ async function handleRequest(request, env, ctx) {
const isGitLFS = isGitLFSRequest(request, new URL(request.url));
const isHF = isHuggingFaceAPIRequest(request, new URL(request.url));
const responseWithCors = (() => {
const headers = addCorsHeaders(
new Headers(response.headers),
request,
env ? createConfig(env) : CONFIG
);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
})();
return isGit || isGitLFS || isDocker || isAI || isHF
? responseWithCors
: addPerformanceHeaders(responseWithCors, monitor);
? response
: addPerformanceHeaders(response, monitor);
}
export default {
+35 -2
View File
@@ -32,8 +32,41 @@
* @returns {boolean} True if this is an AI inference request
*/
export function isAIInferenceRequest(request, url) {
void request;
return url.pathname.startsWith('/ip/');
// Check for AI inference provider paths (ip/{provider}/...)
if (url.pathname.startsWith('/ip/')) {
return true;
}
// Check for common AI inference API endpoints
const aiEndpoints = [
'/v1/chat/completions',
'/v1/completions',
'/v1/messages',
'/v1/predictions',
'/v1/generate',
'/v1/embeddings',
'/openai/v1/chat/completions'
];
if (aiEndpoints.some(endpoint => url.pathname.includes(endpoint))) {
return true;
}
// Check for AI-specific content types
const contentType = request.headers.get('Content-Type') || '';
if (contentType.includes('application/json') && request.method === 'POST') {
// Additional check for common AI inference patterns in URL
if (
url.pathname.includes('/chat/') ||
url.pathname.includes('/completions') ||
url.pathname.includes('/generate') ||
url.pathname.includes('/predict')
) {
return true;
}
}
return false;
}
/**
+47 -70
View File
@@ -124,66 +124,6 @@ export function getScopeFromUrl(url, effectivePath, platform) {
return scope;
}
/**
* Normalizes Docker Hub official images to the canonical library namespace.
* @param {string} platformKey
* @param {string} repoPath
* @returns {string} Normalized upstream repository path.
*/
function normalizeRepoPath(platformKey, repoPath) {
if (platformKey === 'cr-docker' && repoPath && !repoPath.includes('/')) {
return `library/${repoPath}`;
}
return repoPath;
}
/**
* Resolves the target registry and scope for Docker auth proxy requests.
* @param {URL} url
* @param {{ [key: string]: string }} platforms
* @returns {{ platformKey: string, upstreamScope: string }} Resolved auth target info.
*/
function resolveDockerAuthTarget(url, platforms) {
const scope = url.searchParams.get('scope') || '';
const pathMatch = url.pathname.match(/^\/cr\/([^/]+)\/v2\/auth\/?$/);
let platformKey = pathMatch ? `cr-${pathMatch[1]}` : '';
let repoPath = '';
let upstreamScope = scope;
if (scope) {
const parts = scope.split(':');
if (parts.length >= 3 && parts[0] === 'repository') {
const [, fullRepoPath] = parts;
if (fullRepoPath.startsWith('cr/')) {
for (const key of SORTED_PLATFORMS) {
if (!key.startsWith('cr-')) continue;
const prefix = key.replace(/-/g, '/');
if (fullRepoPath.startsWith(`${prefix}/`)) {
platformKey = key;
repoPath = fullRepoPath.slice(prefix.length + 1);
break;
}
}
} else {
repoPath = fullRepoPath;
}
repoPath = normalizeRepoPath(platformKey, repoPath);
upstreamScope = repoPath ? `repository:${repoPath}:${parts.slice(2).join(':')}` : scope;
}
}
if (!platformKey || !platforms[platformKey]) {
throw new Error('Unsupported registry platform in scope');
}
return { platformKey, upstreamScope };
}
/**
* Creates an unauthorized (401) response for container registry authentication.
*
@@ -222,17 +162,46 @@ export function responseUnauthorized(url) {
* @returns {Promise<Response>} The response (token or error)
*/
export async function handleDockerAuth(request, url, config) {
let target;
try {
target = resolveDockerAuthTarget(url, config.PLATFORMS);
} catch (error) {
// Log internal error details server-side without exposing them to the client
console.error('Failed to resolve Docker auth target:', error);
// Return a generic error response to avoid leaking implementation details
return createErrorResponse('Invalid Docker authentication request', 400);
const scope = url.searchParams.get('scope');
if (!scope) {
return createErrorResponse('Missing scope parameter', 400);
}
const upstreamUrl = config.PLATFORMS[target.platformKey];
// Parse scope to find the target platform and repository
// Format: repository:cr/docker/library/ubuntu:pull
// We need to extract 'cr/docker' as the platform
const parts = scope.split(':');
if (parts.length < 3 || parts[0] !== 'repository') {
// If not a repository scope, or invalid format, we can't easily proxy it
return createErrorResponse('Invalid scope format', 400);
}
const [, fullRepoPath] = parts; // e.g., cr/docker/library/ubuntu
let platformKey = '';
let repoPath = '';
// Find the platform from the start of the repo path
// Try to match 'cr/docker', 'cr/ghcr', etc.
// We need to find which platform prefix matches the start of fullRepoPath
// Uses global SORTED_PLATFORMS which is imported
for (const key of SORTED_PLATFORMS) {
if (!key.startsWith('cr-')) continue;
// Convert key cr-docker to cr/docker for matching
const prefix = key.replace(/-/g, '/');
if (fullRepoPath.startsWith(`${prefix}/`)) {
platformKey = key;
repoPath = fullRepoPath.slice(prefix.length + 1); // +1 for the slash
break;
}
}
if (!platformKey || !config.PLATFORMS[platformKey]) {
return createErrorResponse('Unsupported registry platform in scope', 400);
}
const upstreamUrl = config.PLATFORMS[platformKey];
const authorization = request.headers.get('Authorization');
// 1. Fetch the upstream root (v2) to get the proper realm and service
@@ -256,6 +225,14 @@ export async function handleDockerAuth(request, url, config) {
const wwwAuthenticate = parseAuthenticate(authenticateStr);
// 2. Construct the new scope for the upstream registry
// We replace our prefixed path with the actual repo path
// e.g. repository:cr/docker/library/ubuntu:pull -> repository:library/ubuntu:pull
// However, we also need to respect the service name if possible,
// but usually we just need to fix the repository part of the scope.
const newScope = `repository:${repoPath}:${parts.slice(2).join(':')}`;
// 3. Fetch the token from the upstream realm
return await fetchToken(wwwAuthenticate, target.upstreamScope, authorization || '');
return await fetchToken(wwwAuthenticate, newScope, authorization || '');
}
-53
View File
@@ -20,59 +20,6 @@
* Security utility functions for Xget
*/
/**
* Resolves the allowed CORS origin for the current request.
* @param {Request} request
* @param {import('../config/index.js').ApplicationConfig} config
* @returns {string | null} Allowed origin value for the response, or null if not allowed.
*/
export function resolveAllowedOrigin(request, config) {
const origin = request.headers.get('Origin');
if (!origin) {
return null;
}
const allowedOrigins = config.SECURITY.ALLOWED_ORIGINS;
if (allowedOrigins.includes('*')) {
return '*';
}
return allowedOrigins.includes(origin) ? origin : null;
}
/**
* Applies CORS headers to a response when the request origin is allowed.
* @param {Headers} headers
* @param {Request} request
* @param {import('../config/index.js').ApplicationConfig} config
* @returns {Headers} The same headers object with CORS headers applied when permitted.
*/
export function addCorsHeaders(headers, request, config) {
const allowedOrigin = resolveAllowedOrigin(request, config);
if (!allowedOrigin) {
return headers;
}
headers.set('Access-Control-Allow-Origin', allowedOrigin);
headers.set('Access-Control-Allow-Methods', config.SECURITY.ALLOWED_METHODS.join(', '));
const requestedHeaders = request.headers.get('Access-Control-Request-Headers');
if (requestedHeaders) {
headers.set('Access-Control-Allow-Headers', requestedHeaders);
}
const vary = new Set(
(headers.get('Vary') || '')
.split(',')
.map(value => value.trim())
.filter(Boolean)
);
vary.add('Origin');
headers.set('Vary', Array.from(vary).join(', '));
return headers;
}
/**
* Adds comprehensive security headers to response headers.
*
+33 -47
View File
@@ -92,39 +92,34 @@ function hasAsciiControlChars(value) {
* @returns {boolean} True if this is a container registry operation
*/
export function isDockerRequest(request, url) {
const { pathname } = url;
// Check for container registry API endpoints
if (pathname === '/v2' || pathname === '/v2/' || pathname.startsWith('/v2/')) {
if (url.pathname.includes('/v2/') || url.pathname === '/v2') {
return true;
}
if (pathname.startsWith('/cr/')) {
if (/^\/cr\/[^/]+\/v2(?:\/|$)/.test(pathname)) {
return true;
}
// Check for Docker-specific User-Agent
const userAgent = request.headers.get('User-Agent') || '';
if (userAgent.toLowerCase().includes('docker/')) {
return true;
}
const userAgent = request.headers.get('User-Agent') || '';
if (userAgent.toLowerCase().includes('docker/')) {
return true;
}
// Check for Docker-specific Accept headers
const accept = request.headers.get('Accept') || '';
if (
accept.includes('application/vnd.docker.distribution.manifest') ||
accept.includes('application/vnd.oci.image.manifest') ||
accept.includes('application/vnd.docker.image.rootfs.diff.tar.gzip')
) {
return true;
}
const accept = request.headers.get('Accept') || '';
if (
accept.includes('application/vnd.docker.distribution.manifest') ||
accept.includes('application/vnd.oci.image.manifest') ||
accept.includes('application/vnd.docker.image.rootfs.diff.tar.gzip')
) {
return true;
}
const contentType = request.headers.get('Content-Type') || '';
if (
contentType.includes('application/vnd.docker.distribution.manifest') ||
contentType.includes('application/vnd.oci.image.manifest')
) {
return true;
}
// Check for Docker-specific Content-Type headers (for PUT/POST)
const contentType = request.headers.get('Content-Type') || '';
if (
contentType.includes('application/vnd.docker.distribution.manifest') ||
contentType.includes('application/vnd.oci.image.manifest')
) {
return true;
}
return false;
@@ -133,25 +128,6 @@ export function isDockerRequest(request, url) {
// Re-export for standard usage
export { isAIInferenceRequest, isGitLFSRequest, isGitRequest, isHuggingFaceAPIRequest };
/**
* Computes the allowed methods for a request based on protocol detection.
* @param {Request} request
* @param {URL} url
* @param {import('../config/index.js').ApplicationConfig} config
* @returns {string[]} Allowed HTTP methods for this request shape.
*/
export function getAllowedMethods(request, url, config = CONFIG) {
const isGit = isGitRequest(request, url);
const isGitLFS = isGitLFSRequest(request, url);
const isDocker = isDockerRequest(request, url);
const isAI = isAIInferenceRequest(request, url);
const isHF = isHuggingFaceAPIRequest(request, url);
return isGit || isGitLFS || isDocker || isAI || isHF
? ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']
: config.SECURITY.ALLOWED_METHODS;
}
/**
* Validates incoming requests against security rules.
*
@@ -168,7 +144,17 @@ export function getAllowedMethods(request, url, config = CONFIG) {
* @returns {{valid: boolean, error?: string, status?: number}} Validation result object
*/
export function validateRequest(request, url, config = CONFIG) {
const allowedMethods = getAllowedMethods(request, url, config);
// Allow POST method for Git, Git LFS, Docker, AI inference, and HF API operations
const isGit = isGitRequest(request, url);
const isGitLFS = isGitLFSRequest(request, url);
const isDocker = isDockerRequest(request, url);
const isAI = isAIInferenceRequest(request, url);
const isHF = isHuggingFaceAPIRequest(request, url);
const allowedMethods =
isGit || isGitLFS || isDocker || isAI || isHF
? ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']
: config.SECURITY.ALLOWED_METHODS;
if (!allowedMethods.includes(request.method)) {
return { valid: false, error: 'Method not allowed', status: 405 };
+1 -1
View File
@@ -104,7 +104,7 @@ describe('Security Features', () => {
expect(response.status).not.toBe(500);
}
}
}, 45000);
}, 30000);
it('should reject extremely long paths', async () => {
const longPath = `/gh/${'a'.repeat(3000)}`;
-19
View File
@@ -92,13 +92,6 @@ describe('Xget Core Functionality', () => {
// Should attempt to proxy to conda
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', () => {
@@ -133,18 +126,6 @@ describe('Xget Core Functionality', () => {
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', () => {
+8 -6
View File
@@ -26,7 +26,7 @@ describe('Integration Tests', () => {
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);
expect([200, 301, 302, 404]).toContain(response.status);
}, 60000);
it('should proxy GitLab file requests correctly', async () => {
@@ -152,16 +152,18 @@ describe('Integration Tests', () => {
it('should include performance metrics in all responses', async () => {
const testUrls = [
'https://example.com/gh/test/repo/file.txt',
'https://example.com/gl/test/repo/file.txt',
'https://example.com/hf/test/model/config.json',
'https://example.com/npm/test-package',
'https://example.com/pypi/simple/test/'
'https://example.com/pypi/simple/test/',
'https://example.com/conda/pkgs/main/test.json'
];
const responses = await Promise.all(testUrls.map(url => SELF.fetch(url, { method: 'HEAD' })));
for (const response of responses) {
for (const url of testUrls) {
const response = await SELF.fetch(url, { method: 'HEAD' });
expect(response.headers.get('X-Performance-Metrics')).toBeTruthy();
}
}, 20000);
}, 10000);
});
describe('Content Type Handling', () => {
+36
View File
@@ -235,6 +235,42 @@ describe('Container Registry Support', () => {
});
});
describe('Container Registry Platform Support', () => {
const containerRegistries = [
{ name: 'Docker Hub', prefix: 'cr/docker', expectedStatus: [200, 301, 302, 401, 404, 429] },
{ name: 'Quay.io', prefix: 'cr/quay', expectedStatus: [200, 301, 302, 401, 404, 429] },
{
name: 'Google Container Registry',
prefix: 'cr/gcr',
expectedStatus: [200, 301, 302, 401, 404, 429]
},
{
name: 'Microsoft Container Registry',
prefix: 'cr/mcr',
expectedStatus: [200, 301, 302, 401, 404, 429]
},
{
name: 'GitHub Container Registry',
prefix: 'cr/ghcr',
expectedStatus: [200, 301, 302, 401, 404, 429]
},
{
name: 'Amazon ECR Public',
prefix: 'cr/ecr',
expectedStatus: [200, 301, 302, 401, 404, 429]
}
];
containerRegistries.forEach(({ name, prefix, expectedStatus }) => {
it(`should support ${name} registry`, async () => {
const testUrl = `https://example.com/${prefix}/v2/test/image/manifests/latest`;
const response = await SELF.fetch(testUrl, { method: 'HEAD' });
expect(expectedStatus).toContain(response.status);
}, 10000);
});
});
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
-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');
});
});
-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 { dependencies } = packageJson;
const typedDependencies = /** @type {Record<string, string> | undefined} */ (dependencies);
expect(packageJson.name).toBe('xget');
expect(typedDependencies?.xget).toBeUndefined();
});
});
-4
View File
@@ -330,10 +330,6 @@ describe('Platform Configuration', () => {
});
});
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',
-135
View File
@@ -1,135 +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 { handleDockerAuth } 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('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/');
});
});
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'
});
});
});