From 985c61ed74044126b82b043a674d6dbab31e96dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Felix=20F=C3=B6rtsch?=
Date: Fri, 6 Feb 2026 20:44:53 +0100
Subject: [PATCH] add Cloudflare Setup Service to bypass CORS restrictions
The Cloudflare API blocks direct browser requests due to CORS.
This setup service acts as a proxy, allowing browser-based
worker deployment.
Changes:
- Create cf-setup-service.js worker for CORS proxy
- Update CloudflareService to use setup service API
- Add deployment instructions in workers/README.md
- Improve token creation guide with Zone field explanation
Next steps:
1. Deploy setup service: npx wrangler deploy workers/cf-setup-service.js --name whattoplay-setup
2. Update CF_SETUP_SERVICE_URL in CloudflareService.ts with your URL
3. Commit and push
Co-Authored-By: Claude
---
src/pages/Settings/CloudflareSetupPage.tsx | 14 +-
src/services/CloudflareService.ts | 177 ++++---------
workers/README.md | 109 ++++++++
workers/cf-setup-service.js | 279 +++++++++++++++++++++
4 files changed, 453 insertions(+), 126 deletions(-)
create mode 100644 workers/README.md
create mode 100644 workers/cf-setup-service.js
diff --git a/src/pages/Settings/CloudflareSetupPage.tsx b/src/pages/Settings/CloudflareSetupPage.tsx
index aa79a36..2917248 100644
--- a/src/pages/Settings/CloudflareSetupPage.tsx
+++ b/src/pages/Settings/CloudflareSetupPage.tsx
@@ -173,9 +173,19 @@ export default function CloudflareSetupPage() {
"Use template"
- Schritt 4: Bei "Account Resources" sollte "All
- accounts" ausgewählt sein. Lass alle Einstellungen wie sie sind.
+ Schritt 4: Überprüfe die Permissions:
+
+ -
+ Account Resources: "All accounts" sollte ausgewählt
+ sein
+
+ -
+ Zone Resources: Wähle "All zones" ODER ignoriere
+ dieses Feld (Workers sind Account-level, nicht Zone-level)
+
+
+ Lass alle anderen Einstellungen wie sie sind.
Schritt 5: Scrolle nach unten und klicke auf{" "}
"Continue to summary"
diff --git a/src/services/CloudflareService.ts b/src/services/CloudflareService.ts
index 0377c20..8496131 100644
--- a/src/services/CloudflareService.ts
+++ b/src/services/CloudflareService.ts
@@ -1,9 +1,15 @@
/**
* CloudflareService - API Wrapper für Cloudflare Workers Deployment
* Ermöglicht automatisches Worker Deployment direkt aus der PWA
+ *
+ * Uses a CORS proxy service to bypass browser restrictions when calling
+ * the Cloudflare API. You can deploy your own setup service using
+ * workers/cf-setup-service.js
*/
-const CF_API_BASE = "https://api.cloudflare.com/client/v4";
+// Default setup service URL - you can deploy your own instance
+const CF_SETUP_SERVICE_URL =
+ "https://whattoplay-setup.YOUR-SUBDOMAIN.workers.dev";
export interface CFAccountInfo {
valid: boolean;
@@ -25,64 +31,41 @@ export class CloudflareService {
*/
static async validateToken(apiToken: string): Promise {
try {
- // Get accounts
- const accountsResponse = await fetch(`${CF_API_BASE}/accounts`, {
+ const response = await fetch(CF_SETUP_SERVICE_URL, {
+ method: "POST",
headers: {
- Authorization: `Bearer ${apiToken}`,
+ "Content-Type": "application/json",
},
+ body: JSON.stringify({
+ action: "validate",
+ apiToken,
+ }),
});
- if (!accountsResponse.ok) {
+ if (!response.ok) {
return {
valid: false,
- error: `API Error: ${accountsResponse.status} ${accountsResponse.statusText}`,
+ error: `Setup service error: ${response.status} ${response.statusText}`,
};
}
- const accountsData = await accountsResponse.json();
-
- if (
- !accountsData.success ||
- !accountsData.result ||
- accountsData.result.length === 0
- ) {
- return {
- valid: false,
- error: "No Cloudflare accounts found for this token",
- };
- }
-
- const account = accountsData.result[0];
-
- // Get Workers subdomain
- const subdomainResponse = await fetch(
- `${CF_API_BASE}/accounts/${account.id}/workers/subdomain`,
- {
- headers: {
- Authorization: `Bearer ${apiToken}`,
- },
- },
- );
-
- let subdomain: string | undefined;
-
- if (subdomainResponse.ok) {
- const subdomainData = await subdomainResponse.json();
- subdomain = subdomainData.result?.subdomain || undefined;
- }
+ const data = await response.json();
return {
- valid: true,
- accountId: account.id,
- accountName: account.name,
- subdomain,
+ valid: data.valid,
+ accountId: data.accountId,
+ accountName: data.accountName,
+ subdomain: data.subdomain,
+ error: data.error,
};
} catch (error) {
console.error("Token validation failed:", error);
return {
valid: false,
error:
- error instanceof Error ? error.message : "Token validation failed",
+ error instanceof Error
+ ? error.message
+ : "Failed to connect to setup service",
};
}
}
@@ -108,52 +91,38 @@ export class CloudflareService {
const scriptContent = await scriptResponse.text();
- // Prepare multipart form data
- const formData = new FormData();
-
- formData.append(
- "metadata",
- JSON.stringify({
- main_module: "steam-proxy.js",
- compatibility_date: "2024-01-01",
- bindings: [],
- }),
- );
-
- formData.append(
- "steam-proxy.js",
- new Blob([scriptContent], {
- type: "application/javascript+module",
- }),
- "steam-proxy.js",
- );
-
- // Deploy Worker
- const deployResponse = await fetch(
- `${CF_API_BASE}/accounts/${accountId}/workers/scripts/${scriptName}`,
- {
- method: "PUT",
- headers: {
- Authorization: `Bearer ${apiToken}`,
- },
- body: formData,
+ // Deploy via setup service
+ const response = await fetch(CF_SETUP_SERVICE_URL, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
},
- );
+ body: JSON.stringify({
+ action: "deploy",
+ apiToken,
+ accountId,
+ scriptName,
+ scriptContent,
+ }),
+ });
- const deployData = await deployResponse.json();
-
- if (!deployData.success) {
- const errorMsg = deployData.errors?.[0]?.message || "Deployment failed";
+ if (!response.ok) {
return {
success: false,
- error: errorMsg,
+ error: `Setup service error: ${response.status} ${response.statusText}`,
};
}
- // Get subdomain for Worker URL
- const accountInfo = await this.validateToken(apiToken);
+ const data = await response.json();
- if (!accountInfo.subdomain) {
+ if (!data.success) {
+ return {
+ success: false,
+ error: data.error || "Deployment failed",
+ };
+ }
+
+ if (!data.workerUrl) {
return {
success: false,
error:
@@ -161,58 +130,18 @@ export class CloudflareService {
};
}
- const workerUrl = `https://${scriptName}.${accountInfo.subdomain}.workers.dev`;
-
return {
success: true,
- workerUrl,
+ workerUrl: data.workerUrl,
};
} catch (error) {
console.error("Worker deployment failed:", error);
- return {
- success: false,
- error: error instanceof Error ? error.message : "Deployment failed",
- };
- }
- }
-
- /**
- * Enables workers.dev subdomain (if not already enabled)
- */
- static async enableSubdomain(
- apiToken: string,
- accountId: string,
- subdomain: string,
- ): Promise<{ success: boolean; error?: string }> {
- try {
- const response = await fetch(
- `${CF_API_BASE}/accounts/${accountId}/workers/subdomain`,
- {
- method: "PUT",
- headers: {
- Authorization: `Bearer ${apiToken}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ subdomain }),
- },
- );
-
- const data = await response.json();
-
- if (!data.success) {
- return {
- success: false,
- error: data.errors?.[0]?.message || "Failed to enable subdomain",
- };
- }
-
- return { success: true };
- } catch (error) {
- console.error("Failed to enable subdomain:", error);
return {
success: false,
error:
- error instanceof Error ? error.message : "Failed to enable subdomain",
+ error instanceof Error
+ ? error.message
+ : "Failed to connect to setup service",
};
}
}
diff --git a/workers/README.md b/workers/README.md
new file mode 100644
index 0000000..4fe91da
--- /dev/null
+++ b/workers/README.md
@@ -0,0 +1,109 @@
+# Cloudflare Setup Service
+
+This is a CORS proxy service that allows browser-based deployment of Cloudflare Workers. Deploy this once on your Cloudflare account, and all users can use it to deploy their own workers without CORS restrictions.
+
+## Why is this needed?
+
+The Cloudflare API doesn't allow direct browser requests due to CORS restrictions. This service acts as a proxy, forwarding requests from the browser to the Cloudflare API.
+
+## Security
+
+- **API tokens are NOT stored or logged** - they are only used to validate and deploy workers
+- All communication happens over HTTPS
+- The service only accepts POST requests with specific actions
+- CORS is enabled to allow browser access
+
+## Deployment
+
+### Option 1: Quick Deploy with Wrangler
+
+```bash
+cd workers
+npx wrangler deploy cf-setup-service.js --name whattoplay-setup
+```
+
+This will deploy the service and give you a URL like:
+`https://whattoplay-setup.YOUR-SUBDOMAIN.workers.dev`
+
+### Option 2: Deploy via Dashboard
+
+1. Go to https://dash.cloudflare.com
+2. Navigate to Workers & Pages
+3. Click "Create Application" → "Create Worker"
+4. Name it `whattoplay-setup`
+5. Click "Deploy"
+6. Click "Edit Code"
+7. Replace the default code with the contents of `cf-setup-service.js`
+8. Click "Save and Deploy"
+
+## Configuration
+
+After deployment, update the service URL in your app:
+
+1. Open `src/services/CloudflareService.ts`
+2. Replace `YOUR-SUBDOMAIN` with your actual subdomain:
+ ```typescript
+ const CF_SETUP_SERVICE_URL =
+ "https://whattoplay-setup.YOUR-SUBDOMAIN.workers.dev";
+ ```
+3. Commit and deploy your app
+
+## API
+
+### POST /
+
+**Validate Token:**
+
+```json
+{
+ "action": "validate",
+ "apiToken": "your-cf-api-token"
+}
+```
+
+Response:
+
+```json
+{
+ "valid": true,
+ "accountId": "...",
+ "accountName": "...",
+ "subdomain": "..."
+}
+```
+
+**Deploy Worker:**
+
+```json
+{
+ "action": "deploy",
+ "apiToken": "your-cf-api-token",
+ "accountId": "your-account-id",
+ "scriptName": "worker-name",
+ "scriptContent": "export default { async fetch(...) { ... } }"
+}
+```
+
+Response:
+
+```json
+{
+ "success": true,
+ "workerUrl": "https://worker-name.your-subdomain.workers.dev"
+}
+```
+
+## Usage Limits
+
+Cloudflare Workers Free Tier:
+
+- 100,000 requests/day
+- 10ms CPU time per request
+
+This should be more than enough for personal use. If you expect heavy usage, consider upgrading to the Workers Paid plan ($5/month for 10M requests).
+
+## Sharing
+
+You can share this service URL with other users of your app. They will use your deployment to deploy their own workers on their own Cloudflare accounts.
+
+**Note:** This service does NOT store any data. Each user's worker is deployed on their own Cloudflare account, using their own API token.
diff --git a/workers/cf-setup-service.js b/workers/cf-setup-service.js
new file mode 100644
index 0000000..e5c5a46
--- /dev/null
+++ b/workers/cf-setup-service.js
@@ -0,0 +1,279 @@
+/**
+ * Cloudflare Worker Setup Service
+ *
+ * This worker acts as a CORS proxy for the Cloudflare API, allowing
+ * browser-based deployment of Workers without exposing API tokens.
+ *
+ * Deploy this once on your Cloudflare account, then share the URL.
+ * All users can use this service to deploy their own workers.
+ *
+ * SECURITY: Tokens are NOT stored, only validated and used for deployment.
+ */
+
+export default {
+ async fetch(request, env, ctx) {
+ // Enable CORS
+ const corsHeaders = {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type",
+ };
+
+ // Handle CORS preflight
+ if (request.method === "OPTIONS") {
+ return new Response(null, { headers: corsHeaders });
+ }
+
+ // Only allow POST
+ if (request.method !== "POST") {
+ return new Response(JSON.stringify({ error: "Method not allowed" }), {
+ status: 405,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ });
+ }
+
+ try {
+ const { action, apiToken, accountId, scriptName, scriptContent } =
+ await request.json();
+
+ // Validate required fields
+ if (!action || !apiToken) {
+ return new Response(
+ JSON.stringify({ error: "Missing required fields" }),
+ {
+ status: 400,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ // Route to appropriate handler
+ if (action === "validate") {
+ return await handleValidateToken(apiToken, corsHeaders);
+ } else if (action === "deploy") {
+ if (!accountId || !scriptName || !scriptContent) {
+ return new Response(
+ JSON.stringify({ error: "Missing deployment fields" }),
+ {
+ status: 400,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ }
+ return await handleDeployWorker(
+ apiToken,
+ accountId,
+ scriptName,
+ scriptContent,
+ corsHeaders,
+ );
+ } else {
+ return new Response(JSON.stringify({ error: "Unknown action" }), {
+ status: 400,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ });
+ }
+ } catch (error) {
+ console.error("Setup service error:", error);
+ return new Response(
+ JSON.stringify({
+ error: "Internal server error",
+ message: error.message,
+ }),
+ {
+ status: 500,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ }
+ },
+};
+
+/**
+ * Validates API token and retrieves account info
+ */
+async function handleValidateToken(apiToken, corsHeaders) {
+ try {
+ // Get accounts
+ const accountsResponse = await fetch(
+ "https://api.cloudflare.com/client/v4/accounts",
+ {
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ },
+ },
+ );
+
+ if (!accountsResponse.ok) {
+ const errorData = await accountsResponse.json();
+ return new Response(
+ JSON.stringify({
+ valid: false,
+ error:
+ errorData.errors?.[0]?.message ||
+ `API Error: ${accountsResponse.status}`,
+ }),
+ {
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ const accountsData = await accountsResponse.json();
+
+ if (
+ !accountsData.success ||
+ !accountsData.result ||
+ accountsData.result.length === 0
+ ) {
+ return new Response(
+ JSON.stringify({
+ valid: false,
+ error: "No Cloudflare accounts found for this token",
+ }),
+ {
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ const account = accountsData.result[0];
+
+ // Get Workers subdomain
+ const subdomainResponse = await fetch(
+ `https://api.cloudflare.com/client/v4/accounts/${account.id}/workers/subdomain`,
+ {
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ },
+ },
+ );
+
+ let subdomain;
+ if (subdomainResponse.ok) {
+ const subdomainData = await subdomainResponse.json();
+ subdomain = subdomainData.result?.subdomain || undefined;
+ }
+
+ return new Response(
+ JSON.stringify({
+ valid: true,
+ accountId: account.id,
+ accountName: account.name,
+ subdomain,
+ }),
+ {
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ } catch (error) {
+ return new Response(
+ JSON.stringify({
+ valid: false,
+ error: error.message,
+ }),
+ {
+ status: 500,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ }
+}
+
+/**
+ * Deploys Worker script to Cloudflare
+ */
+async function handleDeployWorker(
+ apiToken,
+ accountId,
+ scriptName,
+ scriptContent,
+ corsHeaders,
+) {
+ try {
+ // Prepare multipart form data
+ const formData = new FormData();
+
+ formData.append(
+ "metadata",
+ JSON.stringify({
+ main_module: "script.js",
+ compatibility_date: "2024-01-01",
+ bindings: [],
+ }),
+ );
+
+ formData.append(
+ "script.js",
+ new Blob([scriptContent], { type: "application/javascript+module" }),
+ "script.js",
+ );
+
+ // Deploy Worker
+ const deployResponse = await fetch(
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}`,
+ {
+ method: "PUT",
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ },
+ body: formData,
+ },
+ );
+
+ const deployData = await deployResponse.json();
+
+ if (!deployData.success) {
+ const errorMsg = deployData.errors?.[0]?.message || "Deployment failed";
+ return new Response(
+ JSON.stringify({
+ success: false,
+ error: errorMsg,
+ }),
+ {
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ // Get subdomain for Worker URL
+ const subdomainResponse = await fetch(
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/subdomain`,
+ {
+ headers: {
+ Authorization: `Bearer ${apiToken}`,
+ },
+ },
+ );
+
+ let workerUrl;
+ if (subdomainResponse.ok) {
+ const subdomainData = await subdomainResponse.json();
+ const subdomain = subdomainData.result?.subdomain;
+ if (subdomain) {
+ workerUrl = `https://${scriptName}.${subdomain}.workers.dev`;
+ }
+ }
+
+ return new Response(
+ JSON.stringify({
+ success: true,
+ workerUrl,
+ }),
+ {
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ } catch (error) {
+ return new Response(
+ JSON.stringify({
+ success: false,
+ error: error.message,
+ }),
+ {
+ status: 500,
+ headers: { ...corsHeaders, "Content-Type": "application/json" },
+ },
+ );
+ }
+}