Skip to main content

CloudflareClient

Struct CloudflareClient 

Source
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.”)

@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<Vec>) helper.”) @yah:handoff(“Grounding for code 1003: NOT in Cloudflare’s own published API reference (checked developers.cloudflare.com’s error-response schema for this endpoint 2026-09-14 — it documents only the generic {code, message} shape, no per-condition table). Grounded instead from convergent third-party Cloudflare API clients that hardcode it for this exact family: vana-com/vana-connect’s gcp.ts comment ‘1003: tunnel not found’, mandar-karhade/dockflare’s error table ‘1003 | Zone not found’, plus independent test fixtures (ratazzi/coulson, MauroDruwel/TunnelDashDesktop) using 1003 for ‘Invalid or missing account id’ / ‘Account not found’. Full sourcing is in the doc comment on is_resource_not_found (cloudflare.rs, just above it). Confirming against a live token was out of scope (ticket forbids live CF calls) and would be worth a follow-up if the sourcing is judged insufficient.”) @yah:handoff(“Left reconciler/ingress.rs:1272-1276 (the abort-before-PUT guard) untouched, per the ticket’s explicit constraint — only read it, did not edit it.”) @yah:handoff(“Sweep for the same latent shape found two more sites with the identical bug: tunnel_dns_records and tunnel_dns_drift (both in cloudflare.rs) each GET a tunnel’s configuration and previously called self.ok(…)? unconditionally, so ANY unconfigured tunnel among many would abort the whole DNS-records listing / drift report with an Err instead of contributing zero ingress hostnames for that tunnel. Fixed both the same way, reusing is_resource_not_found. No other cf_get call site in the file shares this shape — the rest are LIST endpoints (naturally return an empty array when nothing exists, not an error) or are already documented as tolerant (upsert_index_rewrite already treats a missing ruleset entrypoint as empty).”) @yah:handoff(“Added 3 unit tests pinning is_resource_not_found in both directions plus the None case: resource_not_found_classifies_missing_tunnel_config_as_empty (code 1003 -> true), resource_not_found_does_not_classify_auth_error_as_empty (code 10000 -> false), resource_not_found_false_when_no_errors_present. Tests exercise the classifier directly rather than the full async tunnel_configuration end-to-end, because the crate has no HTTP-mocking dev-dependency (checked Cargo.toml dev-dependencies: tempfile, serde_yaml, tokio — no mockito/wiremock) and adding one is a bigger change than this leaf fix warrants; the classifier is the entire decision this fix adds, so it’s what’s under test.”) @yah:verify(“cargo test -p yah-cloud –lib – cloudflare: 29 pass / 0 fail (includes the 3 new tests), no skew reported.”) @yah:verify(“cargo test -p yah-cloud –lib (whole crate): 1200 pass / 2 fail / 4 ignored. The 2 failures (reconciler::mesofact_static::tests::a_mount_extends_the_prefix_and_is_slash_insensitive, bundled_worker_walks_the_route_table) are in mesofact_static.rs, which was already modified and uncommitted by a peer at session start (per git status) and is untouched by this ticket’s diff – confirmed unrelated via git diff –stat on that file.”) @yah:verify(“cargo check -p yah-cloud –lib: clean (1 pre-existing unrelated warning in mesofact_static.rs).”) @yah:verify(“CloudflareClient::ok now surfaces the numeric Cloudflare error code in its returned message (e.g. "Cloudflare error 1003: Configuration for tunnel not found") instead of dropping it, so the FIRST live run of this fix from the reporting camp will either confirm 1003 or name the real code plainly, closing the sourcing gap.”) @yah:assumes(“The not-found classification code (1003) is UNCONFIRMED against a live Cloudflare account — sourced only from convergent third-party API clients (see is_resource_not_found’s doc comment), not from Cloudflare’s own docs or a live call. Disproved by: a live yah 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 : \" (joined with \"; \" for multiple), instead of dropping the code – so the reporting camp’s next live run will show the real code plainly. Added 3 more unit tests (ok_error_message_carries_the_cloudflare_code, ok_error_message_joins_multiple_errors, ok_error_message_falls_back_when_no_errors_present) via CloudflareClient::new(\"test-token\"). Grepped the crate for callers/tests asserting the old bare-message string – none exist outside cloudflare.rs itself; reconciler/ingress.rs:1291 only wraps the error with with_context, doesn’t match on it.”) @yah:verify(“cargo check -p yah-cloud –lib: clean, no errors, after the ok() change. cargo test -p yah-cloud –lib currently fails to COMPILE (19 errors, all 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) -> Result, a thin wrapper around the existing (private) list_tunnels_meta that surfaces just the connector state for one tunnel by id.”) @yah:handoff(“Ordering: lists existing DNS records at the hostname first. If already a correct CNAME with no A/AAAA left beside it, no-op success (idempotent, matches ensure’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.”)

Source

pub fn new(token: String) -> Self

Create a client for the given API token.

Source

pub async fn list_accounts(&self) -> Result<Vec<CfAccountInfo>>

List accounts the token can access. Requires: Account: Read.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub async fn purge_cache_tags( &self, zone_id: &str, tags: &[String], ) -> Result<()>

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.

Source

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.

Source

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).

Source

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).

Source

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).

Source

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.

Source

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).

Source

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.

Source

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).

Source

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).

Source

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).

Source

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>.

Source

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.

Source

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.

Source

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).

Source

pub async fn create_account_token( &self, account_id: &str, zone_id: &str, token_name: &str, grants: &[TokenGrant], ) -> Result<CreateTokenResult>

Mint an account-owned API token under account_id from grants scoped to account_id + zone_id.

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§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert 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>

Convert 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)

Convert &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)

Convert &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 T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Converts 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>

Converts 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)

Converts &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)

Converts &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
where T: Any + Send,

Source§

fn into_any_send(self: Box<T>) -> Box<dyn Any + Send>

Converts Box<Trait> (where Trait: DowncastSend) to Box<dyn Any + Send>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_sync(self: Box<T>) -> Box<dyn Any + Sync + Send>

Converts Box<Trait> (where Trait: DowncastSync) to Box<dyn Any + Send + Sync>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send> ⓘ

Converts Arc<Trait> (where Trait: DowncastSync) to Arc<Any>, which can then be downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Fruit for T
where T: Send + Downcast,

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more