================================================================================ CLOUDFLARE WORKER SETUP GUIDE - CORS Proxy for xlwings Lite ================================================================================ This guide helps set up a Cloudflare Worker as a CORS proxy for xlwings Lite. The worker allows downloading files from GitHub, Google Drive, and Dropbox directly in the browser by adding CORS headers. ONE UNIFIED WORKER - handles both public shareable links AND private repos. No need for separate workers anymore. ================================================================================ SECTION A: INSTRUCTIONS FOR AI ASSISTANTS ================================================================================ --- COPY EVERYTHING FROM HERE TO END OF DOCUMENT --- You are helping a user set up a Cloudflare Worker as a CORS proxy for xlwings Lite. IMPORTANT GUIDELINES FOR YOU (THE AI): 1. YOUR PRIMARY JOB IS TO DO THE DEPLOYMENT FOR THE USER. Do not tell the user "I cannot deploy" or "you need to do this yourself." You CAN and SHOULD deploy the worker for them using the Wrangler CLI. The user just needs to provide you with their Cloudflare API Token, then you do the rest. 2. The user may be completely new to Cloudflare. Guide them gently, one or two steps at a time. Do not overwhelm with too many steps at once. 3. Encourage the user to share screenshots if they get stuck. Many users are doing this for the first time and may not know where to click. With a screenshot, you can point them to exactly where to click. 4. THE ONLY CREDENTIAL NEEDED: One Cloudflare API Token with "Edit Cloudflare Workers" permission. This single token is all that's required - no other tokens or credentials needed. Help the user create it: - Go to: https://dash.cloudflare.com/profile/api-tokens - Click "Create Token" - Use template: "Edit Cloudflare Workers" (gives all needed permissions) - Copy the token (shown only once - they must save it!) - Share the token with you so you can deploy 5. Ask the user: - Do they already have a Cloudflare account? - Do they want YOU to deploy the worker for them? (recommended - offer this) - Or do they prefer to do it themselves using the manual guide? 6. If the user does NOT have a Cloudflare account: - Guide them to: https://dash.cloudflare.com/sign-up - It is FREE - no payment required (100k requests/day free tier) - Custom domain is OPTIONAL - free *.workers.dev subdomain works fine 7. For deploying, there are TWO methods: METHOD A: Wrangler CLI (YOU should use this to deploy for the user) - Requires: Node.js, Cloudflare API token - Command: CLOUDFLARE_API_TOKEN=xxx npx wrangler deploy METHOD B: Cloudflare Dashboard (only if user prefers manual setup) - Go to: https://dash.cloudflare.com -> Workers & Pages - Click "Create" -> "Create Worker" - Paste the worker code - No CLI needed 8. There is ONE unified worker script that handles everything: - Public GitHub releases - Private GitHub repos (with Authorization header) - Google Drive (old & new 2024-2025 domains) - Dropbox shareable links 9. IMPORTANT: Custom domain is NOT required. Every worker gets a free workers.dev subdomain automatically after deployment. 10. Be patient. If something fails, help troubleshoot. Common issues are listed in SECTION F (Troubleshooting) at the end. FOR AI CODERS - DEPLOYMENT PROTOCOL: Step 1: Save worker file and wrangler.toml to a directory Step 2: Deploy with API token (inline env var works best): CLOUDFLARE_API_TOKEN=your_token npx wrangler deploy --config wrangler.toml Step 3: Verify deployment - check output for worker URL Step 4: Add custom domain (optional) via dashboard ================================================================================ SECTION B: QUICK REFERENCE ================================================================================ UNIFIED WORKER: - One script handles all use cases (public + private) - Supports: GitHub, Google Drive, Dropbox - Usage: https://your-worker.workers.dev/?url= - For private repos: Add Authorization header DOMAIN OPTIONS: - FREE: https://github-proxy.yourname.workers.dev (automatic) - OPTIONAL: https://github-proxy.yourdomain.com (if you have domain) REQUIREMENTS: - Cloudflare account (free tier) - For CLI: Node.js + API token ================================================================================ SECTION C: SETUP VIA CLOUDFLARE DASHBOARD (Manual) ================================================================================ STEP 1: Create Cloudflare Account - Go to: https://dash.cloudflare.com/sign-up - No payment required STEP 2: Go to Workers & Pages - Click "Workers & Pages" in left sidebar - Click "Create" -> "Create Worker" STEP 3: Create the Worker - Name it: github-proxy (or any name) - Click "Deploy" to create initial worker - Click "Edit code" STEP 4: Paste the Worker Code - Delete default code - Copy code from SECTION WORKER below - Click "Save and Deploy" STEP 5: Note Your Worker URL - URL shown at top: https://github-proxy.yourusername.workers.dev - This is ready to use! STEP 6: Add Custom Domain (Optional) - Only if you have a domain in Cloudflare - Go to "Triggers" tab -> "Add Custom Domain" ================================================================================ SECTION D: SETUP VIA WRANGLER CLI (For AI Coders) ================================================================================ PREREQUISITES: - Node.js v16+ - Cloudflare API Token with Workers edit permission STEP 1: Get API Token - Go to: https://dash.cloudflare.com/profile/api-tokens - Click "Create Token" - Use template: "Edit Cloudflare Workers" - Copy token (shown only once) STEP 2: Create Files - Save github-proxy-auth-worker.js from SECTION WORKER - Save wrangler.toml from SECTION CONFIG - Put both in same directory STEP 3: Deploy CLOUDFLARE_API_TOKEN=your_token npx wrangler deploy --config wrangler.toml STEP 4: Verify - Check output for worker URL - Visit URL to confirm it works WRANGLER COMMANDS: Deploy: CLOUDFLARE_API_TOKEN=xxx npx wrangler deploy --config wrangler.toml View logs: npx wrangler tail github-proxy-auth Delete worker: npx wrangler delete github-proxy-auth ================================================================================ SECTION E: USING THE PROXY IN XLWINGS LITE ================================================================================ FOR PUBLIC FILES (GitHub releases, Google Drive, Dropbox): - Use proxy URL: https://your-proxy.workers.dev/?url= - No auth header needed FOR PRIVATE GITHUB REPOS: - Same proxy URL - Include header: Authorization: token ghp_xxxxx - PAT needs 'repo' scope URL ENCODING: - Target URL must be URL-encoded - Python: urllib.parse.quote(url, safe='') EXAMPLE: Target: https://github.com/user/repo/releases/download/v1.0/data.parquet Proxy: https://your-proxy.workers.dev/?url=https%3A%2F%2Fgithub.com%2F... ================================================================================ SECTION F: TROUBLESHOOTING ================================================================================ PROXY ERRORS: "Missing url parameter" - The ?url= query parameter is missing - Make sure URL is properly encoded "Invalid URL - domain not allowed" - Target domain not in whitelist - Only GitHub, Google Drive, Dropbox allowed "Failed to fetch" with 404 - File does not exist - For private repos: need Authorization header "Failed to fetch" with 401/403 - Authorization header missing or invalid - Check GitHub PAT has 'repo' scope WRANGLER CLI TROUBLESHOOTING: Problem: "Not authenticated" Solution: Use inline env var format: CLOUDFLARE_API_TOKEN=xxx npx wrangler deploy Note: Windows CMD 'set' command may not work properly. The inline format works on all platforms. Problem: "Missing account ID" Solution: Add to wrangler.toml or let wrangler prompt you. Find account_id: Dashboard -> Workers & Pages -> right sidebar Problem: "Script not found" Solution: Check 'main' path in wrangler.toml matches .js filename Problem: Route not working after deploy Solution: Routes with custom domains must be added via Dashboard: Workers & Pages -> [worker] -> Triggers -> Add Custom Domain The wrangler.toml [[routes]] section requires the zone to be already configured in your Cloudflare account. Problem: "export default" vs "addEventListener" syntax Note: Both work, but addEventListener is more compatible with older wrangler configs. The unified script uses addEventListener. ================================================================================ SECTION WORKER: UNIFIED CORS PROXY (github-proxy-auth-worker.js) ================================================================================ Save as: github-proxy-auth-worker.js This unified worker handles: - Public GitHub releases - Private GitHub repos (with PAT) - Google Drive (old & new domains) - Dropbox shareable links -------------------------------------------------------------------------------- /** * Cloudflare Worker: Unified Multi-Cloud File Proxy * * Purpose: Proxy file downloads to bypass browser CORS restrictions. * Supports both shareable links AND token-based private repo access. * * Supported Services: * - GitHub Releases (public and private repos) * - GitHub Raw Content (public and private repos) * - Google Drive (old and new 2024-2025 domains) * - Dropbox (shareable links and API) * * Features: * - Pure pass-through (no data stored/logged) * - Streams large files (no size limit due to streaming) * - Domain whitelist for security * - Adds CORS headers for browser compatibility * - Forwards Authorization header for private GitHub repos * - GitHub API-based download for private release assets * * Usage: https://your-worker.workers.dev/?url= * * For private repos, include Authorization header: * Authorization: token ghp_xxxxx * * Examples: * - GitHub Public: ?url=https://github.com/user/repo/releases/download/tag/file.ext * - GitHub Private: Same URL + Authorization header * - Google Drive (old): ?url=https://drive.google.com/uc?export=download&id=FILE_ID * - Google Drive (new): ?url=https://drive.usercontent.google.com/download?id=FILE_ID&export=download&confirm=t * - Dropbox: ?url=https://www.dropbox.com/s/abc123/file.ext?dl=1 */ // Allowed domains whitelist (unified - supports all shareable + token access) const ALLOWED_DOMAINS = [ // GitHub "github.com", "api.github.com", "githubusercontent.com", "raw.githubusercontent.com", "github.io", "ghcr.io", // Google Drive (OLD and NEW domains) "drive.google.com", "drive.usercontent.google.com", // NEW 2024-2025 domain for downloads "docs.google.com", "googleusercontent.com", "googleapis.com", // Dropbox "dropbox.com", "www.dropbox.com", "dl.dropboxusercontent.com", "dropboxusercontent.com", "content.dropboxapi.com" ]; addEventListener("fetch", event => { event.respondWith(handleRequest(event.request)); }); function isAllowedUrl(urlString) { try { const url = new URL(urlString); const hostname = url.hostname.toLowerCase(); return ALLOWED_DOMAINS.some(domain => hostname === domain || hostname.endsWith("." + domain) ); } catch { return false; } } function getServiceName(urlString) { try { const url = new URL(urlString); const hostname = url.hostname.toLowerCase(); if (hostname.includes("github") || hostname.includes("githubusercontent")) return "GitHub"; if (hostname.includes("google") || hostname.includes("drive.google")) return "Google Drive"; if (hostname.includes("dropbox")) return "Dropbox"; return "Unknown"; } catch { return "Unknown"; } } /** * Convert GitHub release browser URL to API URL for private repos. * Browser URL: https://github.com/owner/repo/releases/download/tag/filename * API URL: https://api.github.com/repos/owner/repo/releases/tags/tag (to get asset ID) * Then: https://api.github.com/repos/owner/repo/releases/assets/{asset_id} */ function parseGitHubReleaseUrl(urlString) { try { const url = new URL(urlString); if (!url.hostname.includes("github.com")) { return { needsApiLookup: false }; } // Match: /owner/repo/releases/download/tag/filename const match = url.pathname.match(/^\/([^\/]+)\/([^\/]+)\/releases\/download\/([^\/]+)\/(.+)$/); if (match) { return { needsApiLookup: true, owner: match[1], repo: match[2], tag: match[3], filename: match[4] }; } return { needsApiLookup: false }; } catch { return { needsApiLookup: false }; } } /** * For private repos, we need to: * 1. Get the release by tag to find the asset ID * 2. Download the asset using the asset ID with Accept: application/octet-stream */ async function downloadGitHubReleaseAsset(owner, repo, tag, filename, authHeader) { // Step 1: Get release info to find asset ID const releaseUrl = `https://api.github.com/repos/${owner}/${repo}/releases/tags/${tag}`; const releaseResponse = await fetch(releaseUrl, { headers: { "Authorization": authHeader, "Accept": "application/vnd.github+json", "User-Agent": "CloudflareWorker-GitHubProxy/2.0" } }); if (!releaseResponse.ok) { return { ok: false, status: releaseResponse.status, error: `Failed to get release info: ${releaseResponse.status} ${releaseResponse.statusText}` }; } const releaseData = await releaseResponse.json(); // Find the asset by filename const asset = releaseData.assets.find(a => a.name === filename); if (!asset) { return { ok: false, status: 404, error: `Asset '${filename}' not found in release '${tag}'` }; } // Step 2: Download the asset using API URL with octet-stream Accept header const assetResponse = await fetch(asset.url, { headers: { "Authorization": authHeader, "Accept": "application/octet-stream", "User-Agent": "CloudflareWorker-GitHubProxy/2.0" }, redirect: "follow" }); if (!assetResponse.ok) { return { ok: false, status: assetResponse.status, error: `Failed to download asset: ${assetResponse.status} ${assetResponse.statusText}` }; } return { ok: true, response: assetResponse, filename: asset.name, size: asset.size }; } async function handleRequest(request) { // Handle CORS preflight if (request.method === "OPTIONS") { return new Response(null, { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, Accept", "Access-Control-Max-Age": "86400" } }); } // Only allow GET requests if (request.method !== "GET") { return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }); } // Get target URL from query parameter const url = new URL(request.url); const targetUrl = url.searchParams.get("url"); // Return usage info if no URL provided if (!targetUrl) { return new Response(JSON.stringify({ name: "Unified Multi-Cloud File Proxy", version: "3.0", error: "Missing url parameter", usage: { github_public: "?url=https://github.com/user/repo/releases/download/tag/file.ext", github_private: "Same URL + Authorization header: 'token ghp_xxxxx'", gdrive_old: "?url=https://drive.google.com/uc?export=download&id=FILE_ID", gdrive_new: "?url=https://drive.usercontent.google.com/download?id=FILE_ID&export=download&confirm=t", dropbox: "?url=https://www.dropbox.com/s/abc123/file.ext?dl=1" }, supported: ["GitHub Releases (public/private)", "Google Drive (old & new domains)", "Dropbox"] }), { status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }); } // Security: Only allow whitelisted domains if (!isAllowedUrl(targetUrl)) { return new Response(JSON.stringify({ error: "Invalid URL - domain not allowed", allowed: ["github.com", "drive.google.com", "drive.usercontent.google.com", "dropbox.com"], hint: "Only GitHub, Google Drive, and Dropbox URLs are supported" }), { status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }); } const serviceName = getServiceName(targetUrl); const authHeader = request.headers.get("Authorization"); try { // Special handling for GitHub release URLs with authentication (private repos) // For private repos, direct download URLs don't work - need to use GitHub API const releaseInfo = parseGitHubReleaseUrl(targetUrl); if (releaseInfo.needsApiLookup && authHeader && serviceName === "GitHub") { // Use GitHub API to download the asset (works for private repos) const result = await downloadGitHubReleaseAsset( releaseInfo.owner, releaseInfo.repo, releaseInfo.tag, releaseInfo.filename, authHeader ); if (!result.ok) { return new Response(JSON.stringify({ error: result.error, status: result.status, hint: "Make sure the PAT has 'repo' scope for private repositories" }), { status: result.status, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }); } // Build response headers with CORS const headers = new Headers(); headers.set("Access-Control-Allow-Origin", "*"); headers.set("Access-Control-Expose-Headers", "Content-Length, Content-Type, X-Proxy-Service, X-Proxy-Auth, X-Proxy-Method"); headers.set("Content-Type", result.response.headers.get("Content-Type") || "application/octet-stream"); headers.set("X-Proxy-Service", "GitHub"); headers.set("X-Proxy-Auth", "yes"); headers.set("X-Proxy-Method", "api-asset"); headers.set("Cache-Control", "no-store"); if (result.size) { headers.set("Content-Length", String(result.size)); } // Stream the response body directly return new Response(result.response.body, { status: 200, headers: headers }); } // Standard handling for other URLs (public GitHub, Google Drive, Dropbox) const fetchHeaders = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/octet-stream, */*" }; // Forward Authorization header if present (for private GitHub repos) if (authHeader && serviceName === "GitHub") { fetchHeaders["Authorization"] = authHeader; } // Forward Accept header if provided (for GitHub API octet-stream) const acceptHeader = request.headers.get("Accept"); if (acceptHeader) { fetchHeaders["Accept"] = acceptHeader; } // Fetch from target (follows redirects) const response = await fetch(targetUrl, { method: "GET", redirect: "follow", headers: fetchHeaders }); if (!response.ok) { return new Response(JSON.stringify({ error: `Failed to fetch from ${serviceName}`, status: response.status, statusText: response.statusText, hint: response.status === 404 ? "File not found or private repo requires Authorization header" : undefined }), { status: response.status, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }); } // Build response headers with CORS const headers = new Headers(); headers.set("Access-Control-Allow-Origin", "*"); headers.set("Access-Control-Expose-Headers", "Content-Length, Content-Type, X-Proxy-Service, X-Proxy-Auth"); headers.set("Content-Type", response.headers.get("Content-Type") || "application/octet-stream"); headers.set("X-Proxy-Service", serviceName); headers.set("X-Proxy-Auth", authHeader ? "yes" : "no"); const contentLength = response.headers.get("Content-Length"); if (contentLength) { headers.set("Content-Length", contentLength); } // Forward Content-Disposition if present const contentDisposition = response.headers.get("Content-Disposition"); if (contentDisposition) { headers.set("Content-Disposition", contentDisposition); } // No caching to ensure fresh data headers.set("Cache-Control", "no-store"); // Stream the response body directly (no buffering = no size limit) return new Response(response.body, { status: 200, headers: headers }); } catch (error) { return new Response(JSON.stringify({ error: "Proxy error", service: serviceName, message: error.message }), { status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" } }); } } -------------------------------------------------------------------------------- ================================================================================ SECTION CONFIG: WRANGLER CONFIG (wrangler.toml) ================================================================================ Save as: wrangler.toml (in same directory as the .js file) -------------------------------------------------------------------------------- name = "github-proxy-auth" main = "github-proxy-auth-worker.js" compatibility_date = "2024-01-01" # Custom domain routing (optional - configure after first deploy) # Uncomment and edit if you have a custom domain: # [[routes]] # pattern = "github-proxy-auth.yourdomain.com/*" # zone_name = "yourdomain.com" # Enable logging for debugging [observability.logs] enabled = true -------------------------------------------------------------------------------- ================================================================================ SECTION G: API TOKEN CREATION ================================================================================ To deploy via CLI, you need a Cloudflare API token. HOW TO CREATE: 1. Go to: https://dash.cloudflare.com/profile/api-tokens 2. Click "Create Token" 3. Use template "Edit Cloudflare Workers" OR create custom: - Account | Workers Scripts | Edit - Zone | Workers Routes | Edit (only if using custom domain) 4. Copy token immediately (shown only once) TOKEN FORMAT: Looks like: yhhLd_ILqKmwh2Gd3f3XCbzF6f49Jn51961nMYqm Does NOT start with "Bearer " - that's added by API calls USING THE TOKEN: Best method (works on all platforms): CLOUDFLARE_API_TOKEN=your_token npx wrangler deploy Alternative (Windows PowerShell): $env:CLOUDFLARE_API_TOKEN="your_token" npx wrangler deploy ================================================================================ SECTION H: REFERENCE ================================================================================ Example deployment (tigzig.com): Dashboard: https://dash.cloudflare.com Zone ID: db56962f98d6fa7a6349b7f67557713c Deployed: https://github-proxy-auth.tigzig.com Replace with your own domain/Zone ID for your deployment. ================================================================================ END OF DOCUMENT ================================================================================