Skip to main content

Module cloudflare

Module cloudflare 

Source
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_publish and reuses the existing s3_sign helper. 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 –zone ’ so a fresh CF account is one command. The policy template is the checked-in artifact CF won’t let you save dashboard-side”) @yah:gotcha(“Bootstrap credential needs ONLY API Tokens:Edit — VALIDATED live: the created token is bounded by the account’s own access, not the bootstrap token’s perms, so a minimal bootstrap suffices. Brand-new account still needs one such token minted manually (or Global Key) once.”) @yah:gotcha(“TRAP — two Transform Rules groups: ‘Transform Rules Write’ (ae16e88b…) is ACCOUNT-scoped = WRONG. upsert_index_rewrite hits /zones/{id}/rulesets so it needs ZONE-scoped ‘Zone Transform Rules Write’. Zone Read + Cache Purge are also zone-scoped, not account-scoped.”) @yah:gotcha(“Permission-group IDs (global constants, validated 2026-05-26): acct-scoped Account Settings Read=c1fde68c7bcc44588cbb6ddbc16d6480, Workers R2 Storage Write=bf7481a1826f439697cb59a20b22293e; zone-scoped Zone Read=c8fed203ed3043cba015a93ad1616f1f, Zone Transform Rules Write=0ac90a90249747bca6b047d97f0803e9, Cache Purge=e17beae8b8cb423a99b1730f21238bed”) @yah:gotcha(“account-owned token verify = GET /accounts/{id}/tokens/verify; /user/tokens/verify returns success:false for account-owned tokens (NOT a failure — seen in live test)”) @yah:assumes(“VALIDATED live 2026-05-26: POST /accounts/{id}/tokens with the 2-block policy created ‘yah-mesofact-static-yahdev’ which lists R2 buckets successfully. R2 object upload still uses SEPARATE S3 keys (SigV4), not this management token.”) @yah:handoff(“Implemented + verified live end-to-end. CloudflareClient gained create_account_token (cloudflare.rs) + list_permission_group_ids; MESOFACT_STATIC_GRANTS const carries the 5 validated permission-group IDs (account: Account Settings Read, Workers R2 Storage Write; zone: Zone Read, Zone Transform Rules Write, Cache Purge) with group-name→id resolved against the live catalog at create time, baked-in IDs as fallback. Pure helpers build_token_body/resolve_grant_id split account- vs zone-scoped policy blocks (2 unit tests). CLI: ‘yah cloud cf token create –zone [–account ] [–store-slot ] [–name ] [–bootstrap-slot ]’ in cloud.rs handle_cf_token_create — resolves account from –account or .yah/infra/providers/cloudflare.toml, resolves zone name→id, mints account-owned token via POST /accounts/{id}/tokens, stores to keystore (fail-fast on occupied slot, BEFORE minting) or prints once. Bootstrap defaults to cloudflare-api-token slot / $CLOUDFLARE_API_TOKEN.”) @yah:next(“Follow-ups (not in scope): ‘yah cloud cf token revoke ’ (DELETE endpoint already proven), and broader grant presets beyond MESOFACT_STATIC_GRANTS (e.g. + Tunnel:Read/DNS:Read for the Infra panel)”) @yah:verify(“cargo test -p cloud –lib — 207 passed (incl. token_body_splits_scopes_and_resolves_ids, token_body_omits_empty_scope_block)”) @yah:verify(“Live E2E 2026-05-26: ‘yah cloud cf token create –zone yah.dev –store-slot cf-clitest’ minted account-owned token, minted token listed R2 buckets successfully, then DELETE /accounts/{id}/tokens/{id} revoked it cleanly”)

@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 · ’ (pulsing forest dot) for synced+active, ‘inactive’ neutral pill for synced+inactive, ‘degraded’ warn for synced+degraded. fmtUptime() formats ISO → ‘<1m’/‘45m’/‘12h’/‘3d’. TunnelsSection right label changed to ‘N active’ (or ‘N active · M drift’). collectCfSlots test drift() fixture updated with connState: ‘unknown’.”) @yah:verify(“cargo test -p cloud –lib cloudflare — 15 passed (incl. 8 drift unit tests)”) @yah:verify(“cargo check -p desktop — clean (TunnelConnState re-exported + TunnelDriftRow extended)”) @yah:verify(“cd packages/yah/ui && bun test src/components/infra/CloudflarePanel.collectCfSlots.test.ts — 8 pass”) @yah:verify(“cd packages/yah/ui && bun run typecheck — no errors in infra/ or env/ (pre-existing failures elsewhere unchanged)”)

@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: } after fail-fast on workload<->mirror binding-name drift.”)

@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§

CfAccountInfo
A Cloudflare account the API token can access.
CloudflareClient
Cloudflare management API client.
CreateR2BucketResult
Result of creating a Cloudflare R2 bucket.
CreateTokenResult
Result of minting an account-owned API token. value is the secret and is returned by Cloudflare exactly once — store it immediately.
CreateTunnelResult
Result of creating a Cloudflare Named Tunnel.
DnsRecordDetail
One live DNS record with everything a reconciler needs to decide what to change — R859-F1, the read side of CloudflareClient::list_dns_records.
R2BucketInfo
R2 bucket information from the list endpoint.
R2CustomDomain
One R2 custom-domain binding from GET /accounts/{id}/r2/buckets/{bucket}/domains/custom.
TokenGrant
One permission to bake into a minted token: a Cloudflare permission-group display name, the scope it applies at, and a validated fallback ID.
TunnelDnsRecord
One CNAME record a user needs to create in their external DNS registrar to route a hostname through a Cloudflare Tunnel.
TunnelDriftRow
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.
WorkerDeployResult
Result of deploying a Cloudflare Worker script.

Enums§

GrantScope
Resource scope a permission group applies at when building a token policy.
TunnelConnState
Live connection state of a Cloudflare Tunnel connector.
TunnelDriftState
Drift verdict for one tunnel ingress hostname: does live Cloudflare DNS route it to the tunnel’s CNAME target?
WorkerBinding
One binding to inject into a Worker’s env at 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_ingress in reconciler/ingress.rs — a PUT, so it needs the Write/“Edit” group, not the Read one). Group name + fallback ID resolved live 2026-09-15 against GET /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.