Skip to main content

skyzen_cloudflare_admin/
lib.rs

1//! Cloudflare control-plane API primitives used by any project that needs
2//! to manage Cloudflare resources (DNS records, Pages projects, Access
3//! applications, Workers routes, …) from a Rust host tool.
4//!
5//! The crate exposes:
6//!
7//! - [`CloudflareError`] — error type shared across every API call.
8//! - [`CfApiResponse`] / [`CfApiError`] — the common `{success, errors,
9//!   result}` envelope Cloudflare wraps every response in.
10//! - [`TokenSource`] — marker indicating whether the caller holds a proper
11//!   API token (full permissions) or a wrangler-derived OAuth token
12//!   (limited to zones the user owns through dashboard login).
13//!
14//! Concrete high-level clients (DNS record CRUD, Access app lifecycle,
15//! Pages project creation) compose these primitives. This crate is
16//! deliberately kept as a small, stable kernel so that project-specific
17//! orchestration can live in downstream crates without pulling the
18//! primitives in through a private vendoring path.
19
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22
23/// Errors returned by Cloudflare admin operations.
24#[derive(Debug, Error)]
25pub enum CloudflareError {
26    /// An HTTP request failed before Cloudflare returned a structured API response.
27    #[error("HTTP request failed: {0}")]
28    Http(String),
29
30    /// Cloudflare returned one or more API-level errors.
31    #[error("API error: {0}")]
32    Api(String),
33
34    /// The `wrangler` executable was not available on `PATH`.
35    #[error("wrangler not installed. Install with: npm install -g wrangler")]
36    WranglerNotInstalled,
37
38    /// A `wrangler` command exited unsuccessfully.
39    #[error("wrangler command failed: {0}")]
40    WranglerFailed(String),
41
42    /// Wrangler did not have an authenticated Cloudflare session.
43    #[error("not logged in to Cloudflare. Run: wrangler login")]
44    NotLoggedIn,
45
46    /// A response or configuration file could not be parsed.
47    #[error("failed to parse response: {0}")]
48    Parse(String),
49
50    /// The Cloudflare configuration directory could not be found.
51    #[error("config directory not found")]
52    ConfigDirNotFound,
53
54    /// Filesystem IO failed while reading Cloudflare configuration.
55    #[error("IO error: {0}")]
56    Io(#[from] std::io::Error),
57
58    /// JSON serialization or deserialization failed.
59    #[error("JSON error: {0}")]
60    Json(#[from] serde_json::Error),
61
62    /// The provided API token lacks permissions required by the requested endpoint.
63    #[error(
64        "API token missing required permissions for {0}. \
65         Create a token at https://dash.cloudflare.com/profile/api-tokens \
66         with the appropriate scopes and set it as CLOUDFLARE_API_TOKEN."
67    )]
68    InsufficientPermissions(String),
69}
70
71/// Standard Cloudflare API response envelope: `{ success, errors, result }`.
72#[derive(Debug, Deserialize)]
73pub struct CfApiResponse<T> {
74    /// Whether Cloudflare considered the request successful.
75    pub success: bool,
76    /// API errors returned by Cloudflare when `success` is false.
77    #[serde(default)]
78    pub errors: Vec<CfApiError>,
79    /// Successful response payload.
80    pub result: Option<T>,
81}
82
83impl<T> CfApiResponse<T> {
84    /// Convert into `Result<T, CloudflareError>`, joining every error
85    /// message into one string for display.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error when Cloudflare reported failure or omitted the
90    /// expected result payload.
91    pub fn into_result(self) -> Result<T, CloudflareError> {
92        if self.success {
93            self.result
94                .ok_or_else(|| CloudflareError::Api("missing result payload".into()))
95        } else {
96            let joined = self
97                .errors
98                .iter()
99                .map(|e| e.message.clone())
100                .collect::<Vec<_>>()
101                .join(", ");
102            Err(CloudflareError::Api(joined))
103        }
104    }
105}
106
107/// Cloudflare API error entry from a response envelope.
108#[derive(Debug, Deserialize)]
109pub struct CfApiError {
110    /// Cloudflare numeric error code.
111    pub code: i32,
112    /// Human-readable Cloudflare error message.
113    pub message: String,
114}
115
116/// Where the API token came from. Affects which endpoints will succeed
117/// against the authenticated account.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
119pub enum TokenSource {
120    /// Proper API token from `CLOUDFLARE_API_TOKEN` (full permissions).
121    ApiToken,
122    /// OAuth token read from wrangler config. Lacks DNS/Access edit
123    /// permissions; only works against endpoints the interactive login
124    /// scopes covered.
125    WranglerOAuth,
126}