Expand description
Cloudflare management API client — accounts, tunnels, R2 buckets, DNS.
Shared by the desktop Tauri commands, the CLI, and the reconciler.
Callers resolve the API token themselves (keychain, env, vault) and pass
it to CloudflareClient::new; this module never reads credentials.
Two distinct API surfaces:
- Management API (this module) — accounts, R2-bucket CRUD, tunnels, DNS, cache purge.
- R2 object publish — S3 SigV4, lives in
reconciler::r2_publishand reuses the existings3_signhelper. Not part of this module.
@yah:ticket(R320-F12, “yah cloud: mint scoped Cloudflare API token from a policy template (one-command onboarding for a new CF account)”)
@yah:assignee(agent:claude)
@yah:at(2026-05-26T14:57:46Z)
@yah:status(review)
@yah:parent(R320)
@arch:see(.yah/docs/working/W074-cloudflare-infra-provider.md)
@yah:next(“Resolve permission-group UUIDs at runtime from GET /accounts/{id}/tokens/permission_groups — CF references groups by ID not name; the policy template stores names and resolves to IDs at create time”)
@yah:next(“Build the minimal mesofact-static policy: account-scoped block (Account Settings:Read + Workers R2 Storage:Edit) + zone-scoped block (Zone:Read + Transform Rules:Edit + Cache Purge). Keep scope blocks separate — mixing account- and zone-scoped groups in one block fails token-create validation”)
@yah:next(“POST /accounts/{id}/tokens to create an ACCOUNT-OWNED token (DECIDED by user: survives the creating user, correct for a shared tool credential). Print the secret once and offer to store it in the cloudflare-api-token keystore slot”)
@yah:next(“Expose as ‘yah cloud cf token create –account
@yah:ticket(R324-F5, “Tunnel connection status + uptime in the Tunnels table”)
@yah:assignee(agent:claude)
@yah:at(2026-05-26T15:33:48Z)
@yah:status(review)
@yah:phase(P2)
@yah:parent(R324)
@yah:handoff(“Tunnel connection state fully wired end-to-end. Rust: TunnelConnState enum (Active/Inactive/Degraded/Unknown) added to cloud crate; CfTunnel wire struct extended with status + conns_active_at; TunnelMeta private struct carries enriched data; list_tunnels_meta() fetches status in one pass; TunnelDriftRow gained conn_state + conn_since (RFC3339 optional); tunnel_dns_drift() refactored to walk accounts→tunnels_meta→configs in a single list_accounts pass instead of calling tunnel_dns_records() separately (avoids duplicate API round-trip). Re-exported TunnelConnState through provider/mod.rs + cloud/src/lib.rs + desktop cloudflare.rs. TS: TunnelConnState type + connState/connSince on TunnelDriftRow in types.ts. UI: DriftPill now shows ‘connected ·
@yah:ticket(R324-F6, “R2 bucket size + object count + region in Accounts section”) @yah:assignee(agent:claude) @yah:at(2026-05-26T15:33:49Z) @yah:status(review) @yah:phase(P2) @yah:parent(R324) @yah:next(“Extend R2BucketInfo + list_r2_buckets with size/object-count/region; design AccountBlock shows ‘412 MB · 142 obj’. Needs extra R2 (or S3 list) calls per bucket.”) @yah:next(“Render the new fields in CloudflarePanel AccountBlock bucket rows.”) @yah:handoff(“Added location + creation_date to R2BucketInfo (Rust struct + TS interface). Both fields come from the existing GET /accounts/{id}/r2/buckets list call — no extra round-trips. fmtR2Location() maps CF location codes (WEUR/EEUR/WNAM/ENAM/APAC) to short labels. AccountBlock bucket cards now show the region badge on the right when present. Note: bucket size + object count are not available from the CF management API without per-bucket S3 calls (paginated ListObjectsV2 + S3 credentials); deferred to a future ticket.”) @yah:verify(“cargo check -p cloud -p desktop — clean (R2BucketInfo extended, deserialized from BucketEntry, re-exported unchanged)”) @yah:verify(“cd packages/yah/ui && bun run typecheck — no new errors in infra/ or env/”)
@yah:ticket(R419-F1, “Extend deploy_worker_script for r2_bucket bindings”)
@yah:assignee(agent:claude)
@yah:at(2026-06-03T08:02:38Z)
@yah:status(review)
@yah:parent(R419)
@yah:handoff(“Widened deploy_worker_script + build_worker_multipart to typed bindings. New pub enum WorkerBinding<’a> { PlainText { name, text }, R2Bucket { name, bucket_name } } encodes both shapes; multipart metadata.bindings now emits the matching CF wire JSON. Single existing caller (mesofact_static.rs:505) maps its (String,String) plain_text vec into WorkerBinding::PlainText refs — runtime behavior unchanged. F2’s CloudflareWorkerReconciler now has the surface it needs: pass [WorkerBinding::R2Bucket { name: binding_name_from_workload_toml, bucket_name: from_mirror_providers_cache }, …].”)
@yah:verify(“cargo check -p cloud –lib — clean”)
@yah:verify(“cargo test -p cloud –lib provider::cloudflare — 12 passed, incl. multipart_includes_r2_bucket_binding_metadata + multipart_mixes_plain_text_and_r2_bindings”)
@yah:next(“F2 pickup: bind via WorkerBinding::R2Bucket { name: <workload.toml [[bindings]].name>, bucket_name:
@yah:ticket(R893-T22, “MESOFACT_STATIC_GRANTS gains account + zone Analytics Read, so the re-minted token can serve R893-F8”)
@yah:at(2026-09-12T22:27:26Z)
@yah:status(review)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:phase(P4)
@yah:parent(R893)
@yah:next(“Tier: Cleric — a small, well-specified addition to one const array, but the permission-group ids must be resolved against the live Cloudflare catalog rather than guessed, and three stale prose enumerations need correcting in the same pass.”)
@yah:handoff(“Added Account Analytics Read (account, id b89a480218d04ceb98b4fe57ca29dc1f) and Analytics Read (zone, id 9c88f9c5bce24ce7af9a958ba9c504db) to MESOFACT_STATIC_GRANTS in oss/yubaba/crates/cloud/src/provider/cloudflare.rs (now 11 grants: 4 account + 7 zone). Both group names + ids resolved live 2026-09-12 via GET /accounts/3948dc292e724e71b0deefde0ea95999/tokens/permission_groups using the cloudflare-legacy-yah keystore slot (the token the operator granted Analytics on) — never printed, only piped through curl+python filtering.”)
@yah:handoff(“Updated the three stale enumerations named in the ticket: app/yah/cli/src/cloud.rs:1212-1218 doc comment now lists Account Analytics:Read + Analytics:Read; .yah/infra/providers/cloudflare.toml:37-52 gained a third dated paragraph (‘eleven total’) rather than editing the historical seven/nine paragraphs in place; oss/yah-base/crates/keys/src/spec.rs:695 now says ‘eleven’ and its cloudflare.toml consumer line reference corrected from :47 to :53.”)
@yah:handoff(“No mint performed — scope fence respected. cargo test -p yah-cloud –lib: 1205 passed both before and after (baseline established fresh, same run). cargo check -p desktop –lib, -p yah –lib, -p fob –lib all clean (pre-existing warnings only, no new errors).”)
@yah:verify(“cd oss/yubaba && cargo test -p yah-cloud –lib — 1205 passed, 0 failed (matches pre-change baseline)”)
@yah:verify(“cargo check -p desktop –lib — clean”)
@yah:verify(“cargo check -p yah –lib -p fob –lib — clean”)
@yah:assumes(“cloudflare-legacy-yah keystore slot is the same bootstrap token the operator granted Analytics on 2026-09-12 per the ticket description; its successful GET on /tokens/permission_groups (API Tokens:Read/Edit scope) is what proved that grant reached the account, not an independent verification of the Analytics grant itself.”)
@yah:verify(“Leader re-ran the gate independently: cd oss/yubaba && cargo test -p yah-cloud --lib = 1205 passed / 0 failed / 4 ignored, identical to the leader’s own pre-change baseline measured before dispatch. The camp build rail reported "input closure unchanged across the whole run: no skew" on that baseline, so both numbers describe the same tree. Leader also read the grants array directly: Account Analytics Read (account scope) and Analytics Read (zone scope) are present with the live-resolved ids, and the doc comment at cloudflare.rs:382-388 records that the two groups are genuinely distinct and were validated against the catalog rather than inferred from their names.”)
Structs§
- CfAccount
Info - A Cloudflare account the API token can access.
- Cloudflare
Client - Cloudflare management API client.
- Create
R2Bucket Result - Result of creating a Cloudflare R2 bucket.
- Create
Token Result - Result of minting an account-owned API token.
valueis the secret and is returned by Cloudflare exactly once — store it immediately. - Create
Tunnel Result - Result of creating a Cloudflare Named Tunnel.
- DnsRecord
Detail - One live DNS record with everything a reconciler needs to decide what to
change — R859-F1, the read side of
CloudflareClient::list_dns_records. - R2Bucket
Info - R2 bucket information from the list endpoint.
- R2Custom
Domain - One R2 custom-domain binding from
GET /accounts/{id}/r2/buckets/{bucket}/domains/custom. - Token
Grant - One permission to bake into a minted token: a Cloudflare permission-group display name, the scope it applies at, and a validated fallback ID.
- Tunnel
DnsRecord - One CNAME record a user needs to create in their external DNS registrar to route a hostname through a Cloudflare Tunnel.
- Tunnel
Drift Row - One row of the tunnel DNS-drift report: a tunnel ingress hostname paired with whether live Cloudflare DNS routes it to the tunnel, plus the live connector connection state.
- Worker
Deploy Result - Result of deploying a Cloudflare Worker script.
Enums§
- Grant
Scope - Resource scope a permission group applies at when building a token policy.
- Tunnel
Conn State - Live connection state of a Cloudflare Tunnel connector.
- Tunnel
Drift State - Drift verdict for one tunnel ingress hostname: does live Cloudflare DNS route it to the tunnel’s CNAME target?
- Worker
Binding - One binding to inject into a Worker’s
envat deploy time.
Constants§
- MESOFACT_
STATIC_ GRANTS - Minimal permission set for a mesofact-static publish token: see the account,
list/create R2 buckets, deploy Worker scripts, resolve the zone, manage
Worker routes + the index-rewrite Transform Rule, purge the CDN cache, and
read GraphQL Analytics (account + zone, R893-T22 — backs the front-door
latency panel in
app/yah/desktop/src/front_door.rs). - TUNNEL_
EDIT_ GRANTS - Account-scoped grant for a token that only needs to publish and read back
a Cloudflare Tunnel’s ingress configuration (
ensure_tunnel_ingressinreconciler/ingress.rs— a PUT, so it needs the Write/“Edit” group, not the Read one). Group name + fallback ID resolved live 2026-09-15 againstGET /accounts/3948dc292e724e71b0deefde0ea95999/tokens/permission_groups(R912-F1) — Cloudflare’s own catalog calls the group “Cloudflare Tunnel Write”, which is what the dashboard/docs render as “Cloudflare Tunnel: Edit”. Deliberately account-scoped only, no zone grants: a tunnel’s ingress config is an account-level resource, not a per-zone one.