pub struct CloudflareClient { /* private fields */ }Expand description
Cloudflare management API client.
Construct with CloudflareClient::new passing a pre-resolved API token.
The token scope required per method is noted on each method.
Implementations§
Source§impl CloudflareClient
@yah:relay(R907, “A cloudflare-tunnel edge cannot be brought up by apply alone when its tunnel has no configuration yet”)
@yah:at(2026-09-14T19:14:34Z)
@yah:status(open)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:gotcha(“FILED FROM THE noisetable CAMP 2026-09-14 (its R704-T4), where it blocks the staging API door end to end. Filed here rather than there because the fix is entirely in this repo. NOT parented to R845 deliberately: R845 is the right neighbourhood — it landed the edge-first use resolution this camp’s own fix relies on — but it sits in review, and a defect filed onto a review-column relay reaches nobody.”)
@yah:gotcha(“THE CALLER’S ABORT-BEFORE-PUT BEHAVIOUR AT reconciler/ingress.rs:1272-1276 IS CORRECT AND DELIBERATE, and is not what this relay is about. Its doc says a failed API call is never read as "delete every hostname rule", and one tunnel multiplexes every service on a node, so a GET that failed for a real reason must keep aborting before the PUT.”)
impl CloudflareClient
@yah:relay(R907, “A cloudflare-tunnel edge cannot be brought up by apply alone when its tunnel has no configuration yet”)
@yah:at(2026-09-14T19:14:34Z)
@yah:status(open)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:gotcha(“FILED FROM THE noisetable CAMP 2026-09-14 (its R704-T4), where it blocks the staging API door end to end. Filed here rather than there because the fix is entirely in this repo. NOT parented to R845 deliberately: R845 is the right neighbourhood — it landed the edge-first use resolution this camp’s own fix relies on — but it sits in review, and a defect filed onto a review-column relay reaches nobody.”)
@yah:gotcha(“THE CALLER’S ABORT-BEFORE-PUT BEHAVIOUR AT reconciler/ingress.rs:1272-1276 IS CORRECT AND DELIBERATE, and is not what this relay is about. Its doc says a failed API call is never read as "delete every hostname rule", and one tunnel multiplexes every service on a node, so a GET that failed for a real reason must keep aborting before the PUT.”)
@yah:ticket(R907-B1, “ensure_tunnel_ingress cannot bootstrap a tunnel that has no configuration yet”)
@yah:status(review)
@yah:at(2026-09-14T19:34:22Z)
@yah:assignee(agent:bundle-anthropic-miravel)
@yah:parent(R907)
@yah:severity(high)
@yah:next(“Tier: Cleric — a small, well-located error-classification fix in one function, but it sits on a live credentialed write path where the empty-vs-failed distinction has already been reasoned about once and must not be blurred.”)
@yah:next(“THE FIX BELONGS IN THE GET’S CLASSIFICATION, NOT IN THE CALLER: tunnel_configuration should return Ok(json!({})) for the specific not-found error code and Err for everything else. Do NOT loosen the abort-before-PUT guard at reconciler/ingress.rs:1272-1276 — that guard is correct and is what stops a transient API failure being read as "delete every hostname rule" on a tunnel that multiplexes every service on a node.”)
@yah:next(“Worth a sweep while in here: any other cf_get whose "resource has never been created" answer arrives as an error object rather than an empty result has the same latent shape.”)
@yah:verify(“REPRO, MEASURED 2026-09-14 from the noisetable camp: tunnel 1b41587a-7298-4f29-97bf-40d51ed79397, created by yah cloud cf tunnel ensure WITHOUT --hostname so it has never had a configuration PUT, then yah cloud apply --env staging --service noisetable-api. Result: FAILED: reading ingress configuration of tunnel 1b41587a-…: Configuration for tunnel not found, apply stops at first failure, no connector workload is deployed.”)
@yah:verify(“THE CREDENTIAL WAS RULED OUT SEPARATELY, and that is what isolates the defect: with a token lacking Tunnel grants the same call fails with Not authorized instead, and moving the edge onto a Tunnel-capable provider changed the error from Not authorized to Configuration for tunnel not found. Two different failures at the same line means the second one is not an auth problem.”)
@yah:verify(“FIXED when a tunnel with no configuration takes its first ingress rule through yah cloud apply alone, AND a GET that fails for a genuine reason (revoked token) still aborts before any PUT. Both halves — the first alone re-introduces the wholesale-delete risk ingress.rs:1272-1276 exists to prevent.”)
@yah:gotcha(“MECHANISM, TRACED AND READ RATHER THAN INFERRED. CloudflareClient::tunnel_configuration (oss/yubaba/crates/cloud/src/provider/cloudflare.rs:774) already handles an EMPTY configuration correctly further down — .and_then(|r| r.get(\"config\")).cloned().unwrap_or_else(|| json!({})) at :787, plus the non-object guard at :790 whose own comment covers a tunnel whose config reads back as an explicit JSON null. But it checks self.ok(&resp.success, &resp.errors)? FIRST, at :781. Cloudflare answers success: false / Configuration for tunnel not found for a token-form tunnel that has never had a configuration PUT, so that guard fires and the tolerant path at :787 is unreachable for exactly the case it looks like it already covers.”)
@yah:gotcha(“WHY IT IS A BUG AND NOT A MISSING FEATURE: "no configuration exists" is an EMPTY read, not a FAILED one. The function already draws that distinction correctly for a null config body; it simply does not draw it for the API’s own not-found answer. Same fact, two doors — one arrives as result.config = null, the other as an error object.”)
@yah:gotcha(“CONSEQUENCE: the only code path that would CREATE a first configuration is gated behind READING a configuration that cannot exist until it has been created. Any cloudflare-tunnel edge whose tunnel was minted without --hostname is permanently un-bootstrappable through yah cloud apply. The apparent workaround is not one: yah cloud cf tunnel ensure --hostname also upserts a DNS CNAME, so it takes a live hostname dark if the connector is not up yet.”)
@yah:assumes(“NOT VERIFIED FROM THE REPORTING CAMP, because settling it would have meant a write to live Cloudflare: whether the GET returns a 404 status specifically, or HTTP 200 with success: false in the body. Configuration for tunnel not found is the text that surfaced through the with_context at reconciler/ingress.rs:1291. Classify on the Cloudflare error CODE in resp.errors, not on the message string and not on an assumed status — a message match would layer a second lexical guess on the first.”)
@yah:verify(“THE NON-WORKAROUND IS PINNED BY A NEGATIVE RESULT, so nobody re-tries it: run cf tunnel ensure --hostname against a tunnel with no configuration, then yah cloud apply — apply must still fail Configuration for tunnel not found. If a future change makes cf tunnel ensure write config, that is a DIFFERENT fix and this assertion should flip deliberately rather than quietly.”)
@yah:gotcha(“SEVERITY IS HIGHER THAN THIS TICKET FIRST STATED, AND THE OBVIOUS ESCAPE HATCH IS NOT ONE. yah cloud cf tunnel ensure --name <n> --hostname <h> --api-token-slot cloudflare-legacy-yah DOES NOT WRITE INGRESS CONFIGURATION — measured 2026-09-14 against tunnel 1b41587a-7298-4f29-97bf-40d51ed79397, not reasoned from the source. It printed reusing existing 'noisetable-account-staging', then existing connector token preserved in keystore slot, then went STRAIGHT to the DNS upsert. yah cloud apply --env staging --service noisetable-api immediately afterwards failed with the byte-identical Configuration for tunnel not found. Its own --help confirms the scope in one sentence: ensure the tunnel exists, fetch/store the connector token, OPTIONALLY upsert a CNAME — nothing about configuration.”)
@yah:gotcha(“THEREFORE THERE IS NO VERB IN THE CAMP THAT CAN WRITE A TUNNEL’S FIRST INGRESS CONFIGURATION. This is not ‘apply takes an awkward path to a reachable state’ — it is a complete dead end for any tunnel minted without --hostname, with no CLI escape hatch at all. apply is gated behind the GET this ticket describes; cf tunnel ensure never writes config; yah cloud cf exposes only token and tunnel. The only two routes out are (a) fixing the GET’s error classification, i.e. this ticket, or (b) a human creating the configuration by hand in the Cloudflare dashboard. A consumer camp cannot unblock itself.”)
@yah:gotcha(“READ THIS BESIDE THE SIBLING R907-B2 BEFORE DESIGNING THE FIX. The reporting camp hit BOTH of R907’s children on the same door in the same hour, and they compose badly: with no config (this ticket) and no connector, a DNS cutover (B2) has no end date for its dark window. Fixing this one first is what makes the other one safe to sequence.”)
@yah:handoff(“Fixed the root cause: tunnel_configuration (cloudflare.rs) now classifies a failed GET on /cfd_tunnel/{id}/configurations as an EMPTY read (Ok(json!({}))) when the Cloudflare error carries code 1003 (the generic ‘not found’ family), and still returns Err for every other error (auth, rate-limit, transient). Classification is on the numeric code in resp.errors, not the message text, via a new private CloudflareClient::is_resource_not_found(&Option<Vecyah cloud apply against an unconfigured tunnel either (a) succeeding, confirming 1003, or (b) still failing with a message now showing a different numeric code, in which case update CF_ERR_NOT_FOUND in is_resource_not_found to that code.”)
@yah:handoff(“Addendum landed: CloudflareClient::ok now renders every error entry as \"Cloudflare error : missing field build in initializer of MirrorConfig across mesofact_static.rs/pond.rs/local_process.rs/static_asset.rs/mod.rs/mesofact_bundle.rs/cloudflare_worker.rs/ingress.rs) – confirmed zero of those 19 reference provider/cloudflare.rs; this is a live peer’s in-flight MirrorConfig migration (git status showed mesofact_static.rs already modified at session start), not this ticket’s diff. The cloudflare-module test run that passed 29/29 (prior handoff entry) was taken before that peer’s edit progressed to this state; could not re-run cloudflare-scoped tests after the ok() addendum because the whole test binary now fails to link for an unrelated reason. Re-run cargo test -p yah-cloud --lib -- cloudflare once that peer’s migration lands.”)
@yah:handoff(“Addendum complete: CloudflareClient::ok surfaces the numeric Cloudflare error code in its message text instead of discarding it, so the 1003 guess is now self-diagnosing on the first live run. See appended handoff/verify entries for detail and the peer-breakage caveat on re-running the test suite.”)
@yah:verify(“RE-VERIFIED after the peer’s MirrorConfig migration landed: cargo test -p yah-cloud –lib (whole crate) = 1213 pass / 0 fail / 4 ignored, no skew (baseline was 1200/2/4 with the 2 failures being that same peer’s in-flight breakage, now resolved). cargo test -p yah-cloud –lib – cloudflare = 32 pass / 0 fail, includes all 6 new tests (3 for is_resource_not_found, 3 for the ok() addendum).”)
@yah:ticket(R907-B2, “yah cloud cf has no DNS-record verb, so a tunnel hostname whose name already holds A records cannot be moved”)
@yah:status(review)
@yah:at(2026-09-14T20:23:53Z)
@yah:assignee(agent:bundle-anthropic-miravel)
@yah:parent(R907)
@yah:severity(medium)
@yah:next(“Tier: Cleric — small surface to add, but the sequencing semantics are the whole point and a naive delete-then-create verb would be worse than none.”)
@yah:next(“THE ORDERING HAZARD IS THE DESIGN CONSTRAINT, NOT A CAVEAT. Deleting the incumbent A records BEFORE a connector is up takes a live hostname dark, and when the tunnel also has no configuration (sibling R907-B1) that window has NO END DATE — nothing is standing by to answer. So a DNS verb here should not be a thin delete+create wrapper. Prefer a cutover that is atomic, or gated on connector readiness (tunnel has >=1 healthy connector AND an ingress rule for the hostname) before it touches the incumbent record, with the unsafe ordering available only behind an explicit flag that names the outage it buys.”)
@yah:next(“Worth deciding deliberately: whether the verb belongs on yah cloud cf as a general record CRUD, or whether the tunnel cutover should stay a single higher-level operation (cf tunnel cutover --hostname) that owns the safe ordering internally. The second is harder to misuse and matches how cf tunnel ensure already hides the record shape from the caller.”)
@yah:next(“Fix R907-B1 FIRST or in the same change. Sequencing this one alone still leaves a consumer unable to get a configuration onto the tunnel, so the connector-readiness gate above could never be satisfied.”)
@yah:verify(“REPRO: point cf tunnel ensure --name <tunnel> --hostname <h> --api-token-slot cloudflare-legacy-yah at a hostname that currently resolves via A records. Observed: the tunnel-reuse and token-preserve lines print normally, then Error: upserting CNAME <h> → <id>.cfargotunnel.com: An A, AAAA, or CNAME record with that host already exists. The API token is not the issue — cloudflare-legacy-yah carries both Tunnel:Edit and DNS:Edit, and the same invocation’s tunnel half succeeded in the same run.”)
@yah:verify(“FIXED when a hostname on A records can be moved onto a tunnel through the CLI alone, AND the safe-ordering property holds — i.e. an attempt to cut over to a tunnel with no healthy connector either refuses or is explicitly opted into. Both halves: the first alone just automates the outage.”)
@yah:gotcha(“THE SURFACE IS TWO SUBCOMMANDS WIDE. yah cloud cf --help lists exactly token (mint scoped account-owned tokens) and tunnel (idempotent ensure). There is no verb that reads, creates, updates or deletes a DNS record. The only DNS write anywhere in yah cloud cf is the CNAME upsert bolted onto cf tunnel ensure --hostname.”)
@yah:gotcha(“AND THAT UPSERT IS NOT A GENERAL UPSERT. Cloudflare forbids a CNAME coexisting with an A/AAAA record at the same name, so the call fails with An A, AAAA, or CNAME record with that host already exists whenever the incumbent record is not ALREADY a CNAME. The --help text’s promise that it ‘upserts a CNAME … so the public hostname is wired even before the origin daemon comes up’ therefore holds only for a name that is unused or already CNAME’d — precisely NOT the migration case, where a hostname is being moved onto a tunnel from something else.”)
@yah:gotcha(“CONCRETE INSTANCE, MEASURED 2026-09-14 from the noisetable camp: api-staging.noisetable.com holds A records 51.81.85.145 and 45.32.194.254 (its production passway edge, currently serving 200), and cannot be moved to tunnel 1b41587a-7298-4f29-97bf-40d51ed79397 because those two records must be DELETED first. With no DNS verb in the CLI, the only route is hand-editing the Cloudflare dashboard or hand-calling the API — which is exactly the class of action a camp’s own rules put out of bounds for an agent, so the cutover stalls with no in-tool path.”)
@yah:handoff(“Built the higher-level operation as directed: yah cloud cf tunnel cutover --hostname <h> --name <tunnel> [--force-dark-window] (app/yah/cli/src/cloud.rs). No general DNS CRUD exposed on the CLI – the DNS methods it needed (list_dns_records, delete_dns_records_matching, upsert_dns_record) already existed as CloudflareClient methods from prior work (R859-F1); only one new client method was added: CloudflareClient::tunnel_conn_state(account_id, tunnel_id) -> Resultensure’s contract). Otherwise, if A/AAAA records are present, gates on readiness (tunnel_conn_state == Active AND tunnel_configuration’s ingress array contains a rule for the hostname) before deleting them – refuses with a message naming both missing preconditions and the outage it would cause, unless –force-dark-window is passed (which prints an explicit warning naming the outage before proceeding). Deletes only the incumbent A/AAAA records (delete_dns_records_matching, scoped by type+content so a round-robin sibling isn’t touched), then upserts the CNAME.”)
@yah:handoff(“Composes R907-B1 directly: the readiness check’s ingress-rule half calls tunnel_configuration, which is what B1 made tolerant of a never-configured tunnel – so a tunnel with a connector up but no ingress yet correctly reads as not-ready (has_ingress_rule=false) instead of erroring out of the whole cutover attempt.”)
@yah:handoff(“Pure decision logic extracted and unit-tested directly (not through the async handler, matching how the R907-B1 classifier was tested): cutover_already_complete(records, cname_target), tunnel_ready_for_cutover(conn_state, has_ingress_rule), tunnel_ingress_has_hostname(ingress_json, hostname). 7 new tests in a cf_tunnel_cutover_tests module pin: no-op when already correct CNAME alone; not-complete when an A record remains beside the CNAME; not-complete when only A records present; not-complete when CNAME points elsewhere; readiness requires BOTH Active connector state AND an ingress rule (every other TunnelConnState variant fails the gate even with the rule present); ingress-hostname matching is exact and returns false on an empty/malformed config.”)
@yah:handoff(“Plumbing: DnsRecordDetail and TunnelConnState were not previously re-exported through cloud::provider::mod.rs / cloud::lib.rs (only used internally); added both to the existing pub-use lists so app/yah/cli can name them. No new pub surface beyond that plus tunnel_conn_state.”)
@yah:verify(“cargo check -p yah –lib: clean (0 errors), 26 pre-existing warnings none of which touch the new code (spot-checked: the only two cloud.rs warnings, a pre-existing unused mut at line ~16965 and an unused stub fn at line ~3625, predate this change).”)
@yah:verify(“cargo test -p yah –lib – cf_tunnel_cutover_tests: 7 pass / 0 fail, no skew on the final run (an earlier run in this same session hit a transient 92-error compile while a peer’s kg-store/blake3 and slot_table.rs edits were mid-flight – confirmed unrelated by re-running clean after they landed; see gotcha).”)
@yah:verify(“cargo check -p yah-cloud –lib and cargo test -p yah-cloud –lib both re-verified green after this ticket’s additions (1213 pass / 0 fail / 4 ignored) – the new tunnel_conn_state method didn’t regress anything.”)
@yah:verify(“NOT verified live (explicitly out of scope): no live Cloudflare account was hit. The connector-readiness gate’s real-world shape (does list_tunnels_meta’s status field actually read ‘active’ the way TunnelConnState::from_cf_status expects for a freshly-up connector) is asserted only by the existing TunnelConnState tests from prior work, not newly re-verified here.”)
@yah:gotcha(“Mid-session this crate hit two DIFFERENT transient shared-tree compile breaks, both from live peers, both now resolved and neither touching this ticket’s files: (1) oss/yubaba/crates/cloud/src/reconciler/mesofact_static.rs’s MirrorConfig gained a build field mid-session, breaking cargo test -p yah-cloud --lib (test-only, cargo check --lib stayed clean throughout) – landed and reverified 1213/0/4. (2) crates/yah/kg-store/src/camp_config.rs referenced blake3::hash before its Cargo.toml dependency landed, breaking cargo check -p yah --lib entirely for a window – also since resolved. Neither was touched by this session; noted here only so a reviewer re-running the same commands mid-flight doesn’t misattribute a stale failure to R907-B2.”)
@yah:verify(“RE-RUN 2026-09-14 ~13:30 per @Ashguard:rose’s R897 heads-up: an unattributed git stash wiped the tree at 13:09:46, restored ~13:14 (faba56a4 + pop), and this ticket’s two flagged commands (cargo check -p yah –lib, cf_tunnel_cutover_tests) had run inside that window on a prior pass. Confirmed by content first that nothing was lost (git show HEAD still had handle_cf_tunnel_cutover / Cutover variant / cf_tunnel_cutover_tests module, all committed pre-wipe in 30c2c02c per the operator’s own diff verification) – nothing to re-author. Re-ran fresh anyway: cargo check -p yah –lib clean (no skew); cf_tunnel_cutover_tests 7/7 pass (no skew); cargo test -p yah-cloud –lib 1213/0/4 (no skew). All three identical to the pre-wipe results.”)
Sourcepub async fn list_accounts(&self) -> Result<Vec<CfAccountInfo>>
pub async fn list_accounts(&self) -> Result<Vec<CfAccountInfo>>
List accounts the token can access.
Requires: Account: Read.
Sourcepub async fn list_tunnels(
&self,
account_id: &str,
) -> Result<Vec<(String, String)>>
pub async fn list_tunnels( &self, account_id: &str, ) -> Result<Vec<(String, String)>>
List non-deleted Cloudflare Tunnels in account_id as (id, name) pairs.
Requires: Cloudflare Tunnel: Read.
Sourcepub async fn tunnel_conn_state(
&self,
account_id: &str,
tunnel_id: &str,
) -> Result<TunnelConnState>
pub async fn tunnel_conn_state( &self, account_id: &str, tunnel_id: &str, ) -> Result<TunnelConnState>
Live connection state for one tunnel, by id — the connector-readiness
half of a safe DNS cutover (R907-B2). Unknown when the tunnel
doesn’t appear in the account’s tunnel list at all (deleted, wrong
account) rather than erroring, since a caller gating on Active — the
only value that clears the gate — treats every other variant the
same way.
Requires: Cloudflare Tunnel: Read.
Sourcepub async fn tunnel_dns_records(&self) -> Result<Vec<TunnelDnsRecord>>
pub async fn tunnel_dns_records(&self) -> Result<Vec<TunnelDnsRecord>>
Collect CNAME records for all tunnels across all accessible accounts.
Walks accounts → tunnels → ingress configurations. Returns an empty vec when the token has no tunnels or no configured ingress hostnames.
Sourcepub async fn tunnel_dns_drift(&self) -> Result<Vec<TunnelDriftRow>>
pub async fn tunnel_dns_drift(&self) -> Result<Vec<TunnelDriftRow>>
Compute DNS drift for every tunnel ingress hostname, enriched with live connector connection state.
The declared side is the tunnel ingress config; the live side is the
zone’s DNS records. Each ingress hostname is classified:
TunnelDriftState::Synced when a record points at the tunnel’s CNAME
target, Missing when none exists, Mismatch when one points elsewhere.
Connection state (conn_state / conn_since) comes from the status
and conns_active_at fields on the tunnel list response — fetched in the
same pass as the ingress configs to avoid an extra list_accounts round-trip.
Degrades gracefully — a hostname whose zone can’t be resolved or read is
reported ZoneUnknown rather than failing the whole report. Returns an
empty vec when the token has no tunnels or no ingress hostnames.
Requires: Cloudflare Tunnel: Read, Zone: Read, DNS: Read.
Sourcepub async fn list_zones(&self) -> Result<Vec<(String, String)>>
pub async fn list_zones(&self) -> Result<Vec<(String, String)>>
List zones the token can read, as (zone_id, zone_name) pairs.
Requires: Zone: Read.
Sourcepub async fn create_tunnel(
&self,
account_id: &str,
name: &str,
) -> Result<CreateTunnelResult>
pub async fn create_tunnel( &self, account_id: &str, name: &str, ) -> Result<CreateTunnelResult>
Create a new Named Tunnel under account_id and return the connector token.
Requires: Cloudflare Tunnel: Edit.
Sourcepub async fn tunnel_configuration(
&self,
account_id: &str,
tunnel_id: &str,
) -> Result<Value>
pub async fn tunnel_configuration( &self, account_id: &str, tunnel_id: &str, ) -> Result<Value>
Read a tunnel’s remotely-managed configuration body as raw JSON (R594-F11).
Returns the result.config object — the thing a PUT round-trips — or an
empty object when the tunnel has never been configured. Deliberately
untyped: the ingress list is the only key this crate owns, and every
sibling (warp-routing, originRequest, …) must survive a
read-modify-write untouched.
Requires: Cloudflare Tunnel: Read.
Sourcepub async fn put_tunnel_configuration(
&self,
account_id: &str,
tunnel_id: &str,
config: &Value,
) -> Result<()>
pub async fn put_tunnel_configuration( &self, account_id: &str, tunnel_id: &str, config: &Value, ) -> Result<()>
Replace a tunnel’s remotely-managed configuration (R594-F11).
config is the whole config body, not a patch — Cloudflare replaces it
wholesale, which is why callers must GET-merge-PUT rather than PUT a
freshly-built list. See
reconciler::ingress::ensure_tunnel_ingress.
Requires: Cloudflare Tunnel: Edit.
Sourcepub async fn create_r2_bucket(
&self,
account_id: &str,
bucket_name: &str,
) -> Result<CreateR2BucketResult>
pub async fn create_r2_bucket( &self, account_id: &str, bucket_name: &str, ) -> Result<CreateR2BucketResult>
Create a new R2 bucket under account_id.
Requires: Account: Cloudflare R2: Edit.
Sourcepub async fn zone_id_for_name(&self, zone_name: &str) -> Result<String>
pub async fn zone_id_for_name(&self, zone_name: &str) -> Result<String>
Resolve a zone name (e.g. "yah.dev") to its Cloudflare zone ID.
Requires: Zone: Read.
Purge content by cache tags from a zone.
Cache tags must be applied to responses via the Cache-Tag header or
Cloudflare page rules. Returns Ok(()) when all tags are queued for
purge. Requires: Zone: Cache Purge.
Sourcepub async fn upsert_index_rewrite(&self, zone_id: &str) -> Result<()>
pub async fn upsert_index_rewrite(&self, zone_id: &str) -> Result<()>
Upsert the Transform Rule that rewrites GET / → /index.html on the
zone, identified by the stable description tag "yah:static-index".
Idempotent: fetches the existing http_request_transform entrypoint,
drops any prior "yah:static-index" rule, appends the current one,
and PUTs the merged list back. Treats a missing entrypoint (no rules
yet) as an empty list.
Requires: Zone: Transform Rules: Edit.
Sourcepub async fn deploy_worker_script(
&self,
account_id: &str,
script_name: &str,
script_js: &str,
bindings: &[WorkerBinding<'_>],
) -> Result<WorkerDeployResult>
pub async fn deploy_worker_script( &self, account_id: &str, script_name: &str, script_js: &str, bindings: &[WorkerBinding<'_>], ) -> Result<WorkerDeployResult>
Deploy an ES-module Worker script with typed bindings for runtime config.
Each entry in bindings becomes one metadata.bindings[…] declaration in
the upload payload — see WorkerBinding for the supported variants
(plain_text config, R2 bucket references).
Uses a manual multipart/form-data upload (CF Workers API requires multipart when metadata/bindings are attached). Idempotent: re-uploading the same script is safe but costs one CF API round-trip — callers should hash-guard this.
Requires: Workers Scripts: Edit (account-scoped).
Sourcepub async fn upsert_worker_route(
&self,
zone_id: &str,
pattern: &str,
script_name: &str,
) -> Result<()>
pub async fn upsert_worker_route( &self, zone_id: &str, pattern: &str, script_name: &str, ) -> Result<()>
Upsert a Worker route for pattern on zone_id, pointing at script_name.
Idempotent: fetches existing routes, skips PUT/POST when the pattern already points at the right script, updates an existing pattern pointing elsewhere, or creates a new route entry.
Requires: Zone: Workers Routes: Edit (zone-scoped).
Sourcepub async fn upsert_worker_custom_domain(
&self,
account_id: &str,
zone_id: &str,
hostname: &str,
script_name: &str,
) -> Result<()>
pub async fn upsert_worker_custom_domain( &self, account_id: &str, zone_id: &str, hostname: &str, script_name: &str, ) -> Result<()>
Idempotently attach hostname (e.g. cr.yah.dev) as a Workers Custom
Domain on script_name. Custom Domains route every request for the
hostname into the Worker — distinct from a Worker Route, which only
matches a URL pattern within an already-proxied zone.
Walks the existing Custom Domains list first; if hostname is already
bound to script_name on zone_id, returns Ok without an extra PUT.
Otherwise PUTs /accounts/{account_id}/workers/domains, which CF treats
as an upsert keyed on (hostname, environment).
Requires: Workers Scripts: Edit (account-scoped).
Sourcepub async fn delete_r2_bucket(
&self,
account_id: &str,
bucket_name: &str,
) -> Result<()>
pub async fn delete_r2_bucket( &self, account_id: &str, bucket_name: &str, ) -> Result<()>
Delete an R2 bucket under account_id.
Cloudflare’s management API handles non-empty buckets — objects do not
need to be drained first. Returns Ok(()) on success, Err if the
API returns a failure (including “bucket not found” — callers that
need idempotency should probe Self::list_r2_buckets first).
Requires: Account: Cloudflare R2: Edit.
Sourcepub async fn upsert_dns_record(
&self,
zone_id: &str,
name: &str,
record_type: &str,
content: &str,
ttl: u32,
proxied: bool,
) -> Result<String>
pub async fn upsert_dns_record( &self, zone_id: &str, name: &str, record_type: &str, content: &str, ttl: u32, proxied: bool, ) -> Result<String>
Idempotently upsert a DNS record in zone_id. Fetches existing records
with the same name and type: updates the first match if found, creates
a new record otherwise. Returns the provider-issued record ID.
Requires: DNS: Edit (zone-scoped).
Sourcepub async fn upsert_dns_record_matching(
&self,
zone_id: &str,
name: &str,
record_type: &str,
content: &str,
ttl: u32,
proxied: bool,
match_content: bool,
) -> Result<String>
pub async fn upsert_dns_record_matching( &self, zone_id: &str, name: &str, record_type: &str, content: &str, ttl: u32, proxied: bool, match_content: bool, ) -> Result<String>
upsert_dns_record with control over what
counts as “the existing record” — R859-F1.
match_content = false reproduces the original behaviour: the first
record sharing name + record_type is updated in place. That is right
for a single-valued name (one CNAME at cdn.yah.dev) and wrong for a
multi-valued RRset: adding the second A record of a round-robin apex
would rewrite the first one’s content, silently halving the origin set
to one box.
match_content = true keys the lookup on (name, type, content), so
the call means “ensure exactly this record exists” — a no-op update when
it already does, a create when it does not, and never a mutation of a
sibling record at the same name.
Sourcepub async fn list_dns_records(
&self,
zone_id: &str,
name: Option<&str>,
record_type: Option<&str>,
) -> Result<Vec<DnsRecordDetail>>
pub async fn list_dns_records( &self, zone_id: &str, name: Option<&str>, record_type: Option<&str>, ) -> Result<Vec<DnsRecordDetail>>
Read the DNS records in zone_id, optionally narrowed to one name
and/or one record_type — R859-F1, the read half the dns.* catalog
was missing.
Unlike dns_records_named (drift-detection
only, name + content) this returns the record id, type, ttl and
proxy flag, which is what a reconciler needs to decide what to change.
Requires: DNS: Read (zone-scoped).
Sourcepub async fn delete_dns_records(
&self,
zone_id: &str,
name: &str,
record_type: Option<&str>,
) -> Result<u32>
pub async fn delete_dns_records( &self, zone_id: &str, name: &str, record_type: Option<&str>, ) -> Result<u32>
Delete all DNS records in zone_id whose name matches name (and
optionally record_type). Returns the count of records deleted.
A count of 0 is not an error — the records may already have been absent.
Requires: DNS: Edit (zone-scoped).
Sourcepub async fn delete_dns_records_matching(
&self,
zone_id: &str,
name: &str,
record_type: Option<&str>,
content: Option<&str>,
) -> Result<u32>
pub async fn delete_dns_records_matching( &self, zone_id: &str, name: &str, record_type: Option<&str>, content: Option<&str>, ) -> Result<u32>
delete_dns_records narrowed to records
carrying one exact value — R859-F1.
A round-robin apex holds several A records under one name, so
“delete the A records at yah.dev” is not a way to withdraw one
origin: it takes the live ones with it. content = Some(ip) deletes
only the withdrawn member.
Requires: DNS: Edit (zone-scoped).
Sourcepub async fn list_r2_buckets(
&self,
account_id: &str,
) -> Result<Vec<R2BucketInfo>>
pub async fn list_r2_buckets( &self, account_id: &str, ) -> Result<Vec<R2BucketInfo>>
List R2 buckets in account_id.
Requires: Account: Cloudflare R2: Read.
Unlike /accounts and /zones, the R2 list endpoint nests the array
under result.buckets rather than returning result as a bare array,
so it needs CfSingle<R2ListResult> and not CfPage<BucketEntry>.
Sourcepub async fn list_r2_custom_domains(
&self,
account_id: &str,
bucket_name: &str,
) -> Result<Vec<R2CustomDomain>>
pub async fn list_r2_custom_domains( &self, account_id: &str, bucket_name: &str, ) -> Result<Vec<R2CustomDomain>>
List R2 custom-domain bindings on bucket_name.
Requires: Workers R2 Storage: Read (or Write, which implies Read).
The response nests the array under result.domains, mirroring
list_r2_buckets’s result.buckets shape.
Sourcepub async fn add_r2_custom_domain(
&self,
account_id: &str,
bucket_name: &str,
domain: &str,
zone_id: &str,
) -> Result<()>
pub async fn add_r2_custom_domain( &self, account_id: &str, bucket_name: &str, domain: &str, zone_id: &str, ) -> Result<()>
Bind a custom domain to an R2 bucket.
zone_id names the zone that owns domain (resolve via
Self::zone_id_for_name). CF requires it so the CNAME write into
that zone is authorized — even though the caller is the bucket-side
API. CF creates the CNAME automatically; no separate DNS-side call.
Requires: Workers R2 Storage: Edit (account-scoped).
enabled: true activates the binding immediately. CF still has to
validate ownership + provision TLS in the background — the binding
returns success the moment the record is queued, not when the
hostname is fully resolvable. First-time DNS propagation is on the
order of seconds to a minute.
Sourcepub async fn list_permission_group_ids(
&self,
account_id: &str,
) -> Result<BTreeMap<String, String>>
pub async fn list_permission_group_ids( &self, account_id: &str, ) -> Result<BTreeMap<String, String>>
Fetch the account’s permission-group catalog as a name → id map, used
to resolve TokenGrant names before minting a token. Requires the
calling token to carry API Tokens: Read (implied by Write).
Sourcepub async fn create_account_token(
&self,
account_id: &str,
zone_ids: &[String],
token_name: &str,
grants: &[TokenGrant],
) -> Result<CreateTokenResult>
pub async fn create_account_token( &self, account_id: &str, zone_ids: &[String], token_name: &str, grants: &[TokenGrant], ) -> Result<CreateTokenResult>
Mint an account-owned API token under account_id from grants
scoped to account_id + every zone in zone_ids (zone-scoped grants
apply to all of them; at least one is required).
Resolves each grant’s permission-group name against the live catalog
(falling back to its baked-in ID), groups the IDs into account- and
zone-scoped policy blocks, and POSTs to /accounts/{id}/tokens.
Requires the calling token to carry API Tokens: Write — but the
minted token is bounded by the account’s access, not the calling
token’s, so the caller may hold only API Tokens: Write.
The returned CreateTokenResult::value is the secret — Cloudflare
reveals it only here.
Auto Trait Implementations§
impl !RefUnwindSafe for CloudflareClient
impl !UnwindSafe for CloudflareClient
impl Freeze for CloudflareClient
impl Send for CloudflareClient
impl Sync for CloudflareClient
impl Unpin for CloudflareClient
impl UnsafeUnpin for CloudflareClient
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be
downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further
downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSend for T
impl<T> DowncastSend for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<T> ErasedDestructor for Twhere
T: 'static,
impl<T> Fruit for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more