From 907f5deb5fcc0af845858bd635584d0002a3a168 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 18 Jun 2026 20:16:19 +0000 Subject: [PATCH] Add Cloudflare integration adapter with real zone status Implements testConnection and listResources against the Cloudflare API, reporting the configured zone's real status (active/pending/etc) as a resource. Fixed a bug where non-2xx responses with non-JSON bodies (e.g. invalid zone ID) threw inside the JSON parse instead of failing cleanly. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF --- backend/src/integrations/cloudflare.ts | 46 ++++++++++++++++++++++++++ backend/src/integrations/registry.ts | 3 +- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 backend/src/integrations/cloudflare.ts diff --git a/backend/src/integrations/cloudflare.ts b/backend/src/integrations/cloudflare.ts new file mode 100644 index 0000000..0480212 --- /dev/null +++ b/backend/src/integrations/cloudflare.ts @@ -0,0 +1,46 @@ +import type { IntegrationAdapter, Resource } from './types.js' + +interface CloudflareZoneResponse { + success: boolean + result?: { name: string; status: string } + errors?: { message: string }[] +} + +export const cloudflare: IntegrationAdapter = { + async testConnection(config, secrets) { + const zoneId = config.zoneId + const apiKey = secrets.apiKey + if (!zoneId) return { ok: false, message: 'Missing zone ID' } + if (!apiKey) return { ok: false, message: 'Missing API token' } + try { + const res = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}`, { + headers: { Authorization: `Bearer ${apiKey}` }, + }) + if (!res.ok) return { ok: false, message: `HTTP ${res.status}` } + const body = (await res.json()) as CloudflareZoneResponse + if (!body.success) return { ok: false, message: body.errors?.[0]?.message ?? 'Request failed' } + return { ok: true, message: 'Connected' } + } catch (err) { + return { ok: false, message: err instanceof Error ? err.message : 'Connection failed' } + } + }, + + async listResources(config, secrets): Promise { + const zoneId = config.zoneId + const apiKey = secrets.apiKey + if (!zoneId || !apiKey) return [] + const res = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}`, { + headers: { Authorization: `Bearer ${apiKey}` }, + }) + if (!res.ok) return [] + const body = (await res.json()) as CloudflareZoneResponse + if (!body.success || !body.result) return [] + return [ + { + name: body.result.name, + status: body.result.status === 'active' ? 'healthy' : body.result.status === 'pending' || body.result.status === 'initializing' ? 'warning' : 'critical', + detail: `Zone status: ${body.result.status}`, + }, + ] + }, +} diff --git a/backend/src/integrations/registry.ts b/backend/src/integrations/registry.ts index 739baa5..2f9f0b6 100644 --- a/backend/src/integrations/registry.ts +++ b/backend/src/integrations/registry.ts @@ -3,6 +3,7 @@ import { uptimeKuma } from './uptimeKuma.js' import { docker } from './docker.js' import { proxmox } from './proxmox.js' import { netbird } from './netbird.js' +import { cloudflare } from './cloudflare.js' const notImplemented: IntegrationAdapter = { async testConnection() { @@ -15,7 +16,7 @@ export const adapterRegistry: Record = { docker, proxmox, netbird, - cloudflare: notImplemented, + cloudflare, aws: notImplemented, weather: notImplemented, }