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}