/packs / cloudflare

cloudflare

Cloudflare itself, in the workspace: accounts, zones, Workers and mail rules read; Workers uploaded, secrets put, rules edited.

  • integration
  • sources
  • Cloudflare

As of yardr at commit 67389f7, 11 Nov 2026. The pack's own files keep the author's words: issue · bead, repo · depot, workspace · yard, agents · crew, agent · crew member.

01 / apply it

yardr pack apply cloudflare

yardr carries this pack in its binary; the name is enough. yardr pack check cloudflare shows what it would change first. It goes into the workspace itself, and into no repo · depot.

yardr pack apply https://yardr.dev/packs/cloudflare/

From this site, as the files below.

02 / the files

Every file, as it ships.

# Cloudflare as a pack of kind source: the integration, imported into
# the yard and into no depot. What Cloudflare is here: the declared
# source cloudflare, which lists the account, its zones, its Workers
# scripts and its Email Routing rules and addresses as wrangler reads
# them, and uploads Workers, puts their secrets and makes or edits
# Email Routing rules as wrangler writes them, each write on its own
# card.
#
#   yardr pack apply cloudflare
#
# The source is named cloudflare: yardr knows Cloudflare as this pack
# alone.
name: cloudflare
api: 2
kind: source
setup: SETUP.md

sources:
  cloudflare:
    declaration: cloudflare.yaml

# Suggested grants: named sets of the source's calls, printed numbered
# by pack show. Suggesting grants nothing: the operator grants them,
# and terminal pack apply ignores them.
presets:
  read:
    about: List accounts, zones, Workers scripts and Email Routing rules and addresses; change nothing
    grants:
      cloudflare: [read]
  deploy:
    about: Read as read does, and upload Workers, put Worker secrets and make or edit Email Routing rules
    grants:
      cloudflare: [read, worker_upload, worker_secret, email_rule, email_rule_edit]
# Cloudflare as a declared source: what wrangler reads of the user's
# own account, through the Cloudflare API v4: the token verified, the
# token's own scopes, the accounts, the zones, the Workers scripts of
# an account, and a zone's Email Routing rules with an account's
# destination addresses; and what wrangler writes there: a Worker
# script uploaded from a file, a Worker secret whose value the yard
# reads from a secret file at call time, and an Email Routing rule
# made or edited to send one address to a Worker. Each write is its
# own update call, proposed and approved on its own card.
#
# Checked against developers.cloudflare.com/api on 29 October 2026:
# the base is https://api.cloudflare.com/client/v4 and a token goes
# as Authorization: Bearer; GET /user/tokens/verify answers
# {success, result:{id, status}} and GET /user/tokens/{token_id} the
# token with its policies, each policy naming its permission_groups;
# GET /accounts and GET /zones answer {success, result, result_info};
# GET /accounts/{account_id}/workers/scripts the account's scripts;
# GET /zones/{zone_id}/email/routing/rules the zone's rules and
# GET /accounts/{account_id}/email/routing/addresses the account's
# destination addresses, in the same envelope. An account's and a
# zone's id are 32 hex characters, as the API returns them.
#
# GET /accounts is the one call whose reference page names no
# API-token security and no permission group: that page is
# incomplete, and the contract is wrangler, which this pack reads
# as. Wrangler's whoami and its account picker call GET /accounts
# with the active OAuth or API token, sent as Bearer, and /accounts
# is the listing its Account API Tokens fall back to when
# /memberships refuses them (9109): workers-sdk PR #13770, in
# wrangler 4.89.0. So the accounts read sends the token as Bearer
# like every other call here. The scopes the token carries are read
# back after setup by the token call, beside the minimum SETUP.md
# names; nothing refuses a token before it is stored, since the
# setup card has no place to run such a check yet.
#
# The token is the source's secret: the setup card asks for it unseen
# and the yard keeps it in the source's env file alone, handed to each
# call under CLOUDFLARE_API_TOKEN and sent as the header below. It
# stands in no call's parameters, so no call, card or log names it.
#
# A command is a list, run without a shell. A path that does not start
# with / is a file beside this one, which `yardr source add` copies
# into the yard.
name: cloudflare
about: Cloudflare accounts, zones, Workers scripts and Email Routing rules and addresses, read as wrangler reads them; and Workers uploaded, their secrets put, and Email Routing rules made or edited, as wrangler writes them.

# The name of the variable, never the value. A command gets it in its
# environment under this name, and gets no other variable of the yard.
credential:
  env: CLOUDFLARE_API_TOKEN
  header: "Authorization: Bearer {credential}"

base: https://api.cloudflare.com/client/v4
calls:
  verify:
    kind: read
    about: Verify the API token and report its status
    request:
      method: GET
      path: /user/tokens/verify
    answer: { select: [success, result] }
  token:
    kind: read
    about: Read one API token with the scopes Cloudflare reports for it
    params:
      token_id: { type: string, pattern: "^[A-Za-z0-9_-]{1,64}$", max: 64, log: true }
    request:
      method: GET
      path: /user/tokens/{token_id}
    answer: { select: [success, result], max: 262144 }
  accounts:
    kind: read
    about: List the accounts the token can read
    request:
      method: GET
      path: /accounts
    answer: { select: [success, result, result_info], max: 262144 }
  zones:
    kind: read
    about: List the zones the token can read
    request:
      method: GET
      path: /zones
    answer: { select: [success, result, result_info], max: 262144 }
  workers:
    kind: read
    about: List an account's Workers scripts
    params:
      account: { type: string, pattern: "^[0-9a-f]{32}$", max: 32, log: true }
    request:
      method: GET
      path: /accounts/{account}/workers/scripts
    answer: { select: [success, result, result_info], max: 262144 }
  email_rules:
    kind: read
    about: List a zone's Email Routing rules
    params:
      zone: { type: string, pattern: "^[0-9a-f]{32}$", max: 32, log: true }
    request:
      method: GET
      path: /zones/{zone}/email/routing/rules
    answer: { select: [success, result, result_info], max: 262144 }
  email_addresses:
    kind: read
    about: List an account's Email Routing destination addresses
    params:
      account: { type: string, pattern: "^[0-9a-f]{32}$", max: 32, log: true }
    request:
      method: GET
      path: /accounts/{account}/email/routing/addresses
    answer: { select: [success, result, result_info], max: 262144 }
  worker_upload:
    kind: update
    about: Upload a Worker script from a file, as one module
    params:
      account: { type: string, pattern: "^[0-9a-f]{32}$", max: 32, log: true }
      name: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$", max: 64, log: true }
      script: { type: file, max: 10485760 }
    request:
      method: PUT
      path: /accounts/{account}/workers/scripts/{name}
      body: { "worker.js": "{script}", metadata: { main_module: "worker.js" } }
    answer: { select: [success, result], max: 262144 }
  worker_secret:
    kind: update
    about: Put one secret of a Worker, its value read from a secret file
    params:
      account: { type: string, pattern: "^[0-9a-f]{32}$", max: 32, log: true }
      name: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$", max: 64, log: true }
      secret: { type: string, pattern: "^[A-Za-z][A-Za-z0-9_]{0,63}$", max: 64, log: true }
      value: { type: secret-file, max: 8192 }
    request:
      method: PUT
      path: /accounts/{account}/workers/scripts/{name}/secrets
      body: { name: "{secret}", text: "{value}", type: "secret_text" }
    answer: { select: [success, result], max: 262144 }
  email_rule:
    kind: update
    about: Make the Email Routing rule that sends one address to a Worker
    params:
      zone: { type: string, pattern: "^[0-9a-f]{32}$", max: 32, log: true }
      address: { type: string, pattern: "^[^@[:space:]]+@[^@[:space:]]+$", max: 90, log: true }
      worker: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$", max: 64, log: true }
    request:
      method: POST
      path: /zones/{zone}/email/routing/rules
      body: { name: "{address} to {worker}", enabled: true, matchers: [{ type: "literal", field: "to", value: "{address}" }], actions: [{ type: "worker", value: ["{worker}"] }] }
    answer: { select: [success, result], max: 262144 }
  email_rule_edit:
    kind: update
    about: Edit the standing Email Routing rule to send one address to a Worker
    params:
      zone: { type: string, pattern: "^[0-9a-f]{32}$", max: 32, log: true }
      rule: { type: string, pattern: "^[A-Za-z0-9_-]{1,32}$", max: 32, log: true }
      address: { type: string, pattern: "^[^@[:space:]]+@[^@[:space:]]+$", max: 90, log: true }
      worker: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$", max: 64, log: true }
    request:
      method: PUT
      path: /zones/{zone}/email/routing/rules/{rule}
      body: { name: "{address} to {worker}", enabled: true, matchers: [{ type: "literal", field: "to", value: "{address}" }], actions: [{ type: "worker", value: ["{worker}"] }] }
    answer: { select: [success, result], max: 262144 }
check: verify
# Setting up Cloudflare

The pack wires Cloudflare into the yard as the declared source
`cloudflare`: the user's own account read as wrangler reads it —
accounts, zones, Workers scripts, Email Routing rules and destination
addresses — and written to as wrangler writes it: a Worker uploaded
from a file, a Worker secret put from a secret file, and an Email
Routing rule made or edited to send one address to a Worker, each
write proposed and approved on its own card (step 5).

1. **The API token, in Cloudflare.** My Profile → API Tokens →
   Create Token → Create Custom Token (or the "Edit Cloudflare
   Workers" template, trimmed to this list), with exactly these
   minimum scopes, on one account and one zone, plus the user-scoped
   permission needed to read the token's own policy:
   - **API Tokens Read** (User; needed for GET
     `/user/tokens/<token_id>` to report this token's scopes)
   - **Workers Scripts Edit** (Account)
   - **Email Routing Addresses Read** (Account)
   - **Email Routing Rules Edit** (Zone)
   - **Zone Read** (Zone)

   Account Resources: the one account. Zone Resources: the one zone.
   API Tokens Read is a User permission; it is not an account-wide or
   zone-wide grant.
   The yard reads Workers scripts and destination addresses under the
   account, Email Routing rules under the zone, and zones under the
   Zone Read permission. What the token may do is read back after
   setup, from the token itself (step 4): a scope it lacks shows
   there, beside this list. Copy the
   token once: Cloudflare shows it once.
2. **The setup card.** One card carries the pack's whole setup: the
   pack, the token and the grants. Have a crew member ask for it,
   and approve it, in footplate or in `yardr chat` (`ctrl+o`); a
   person sets up the packs they want, one card each:
   ```sh
   yardr bead ask <bead> "Set up Cloudflare for the yardmaster?" --setup cloudflare --crew yardmaster --recommend 1 --why "reading is the job" --context "The yardmaster reads the account, its zones, Workers scripts and Email Routing as wrangler reads them."
   ```
   The card shows the declaration whole, the token to type unseen,
   and one option per preset. Approving it applies the pack, keeps
   the token as the source's secret in
   `~/.yardr/sources/cloudflare.env` (mode 0600, named in the
   answer, never the value), and grants the preset picked. The
   token is in no call's arguments, then or later.
   By hand it is the operator's to type, starting with the pack:
   ```sh
   yardr pack apply cloudflare
   # applied pack cloudflare to the yard: 1 created, 0 changed, 0 unchanged
   # source cloudflare is declared: 11 calls under https://api.cloudflare.com/client/v4 (yardr source show cloudflare)
   yardr source key cloudflare           # CLOUDFLARE_API_TOKEN, typed unseen
   ```
3. **The grants.** A pack of kind source asks none at import, so
   what a crew member may call is picked on the setup card as one
   preset. The pack's preset, as `yardr pack show cloudflare`
   prints it numbered:
   1. `read`: every read call — the token verified and read with
      its scopes, accounts and zones listed, Workers scripts,
      Email Routing rules and addresses listed — and it covers
      any read call the source is later given; writes nothing.
   2. `deploy`: every read, and the four update calls — a Worker
      uploaded, a Worker secret put, and an Email Routing rule
      made or edited. Whoever holds it makes those calls with no
      card of their own, so offering it makes this setup card a
      strong one: approving it hands those calls out for good, and
      the hosted web asks a fresh sign-in for it.

   By hand it is the operator's to type:
   ```sh
   yardr source grant cloudflare read --crew yardmaster
   ```
4. **Check it, and pick the account and zone.** The check verifies
   the stored token; the reads then name what the token can read:
   ```sh
   yardr source check cloudflare
   # cloudflare: credential: CLOUDFLARE_API_TOKEN is set in ~/.yardr/sources/cloudflare.env
   # cloudflare: check call verify (GET /user/tokens/verify): 200 in …ms, … bytes
   # cloudflare: success: true
   # cloudflare: result: {"id":"<token id>","status":"active"}
   yardr call cloudflare accounts
   yardr call cloudflare zones
   yardr call cloudflare token token_id=<the id verify showed>
   ```
   `token` answers the token's policies: the scopes Cloudflare
   reports for it, to set beside step 1's five. A scope the token
   lacks is the one of the five its policies do not name. Then, with
   the account and zone picked from the lists:
   ```sh
   yardr call cloudflare workers account=<account id>
   yardr call cloudflare email_rules zone=<zone id>
   yardr call cloudflare email_addresses account=<account id>
   ```
   Each answers Cloudflare's envelope after its selection:
   `success`, the `result` list, and `result_info`. A list the
   token may not read answers 403 there, with the call named;
   remake the token with the missing scope (step 1) and give it
   again (step 2).
5. **The writes, each on its own card.** A write to Cloudflare is
   proposed as one call and approved on one card; the card shows
   what the call sends. A crew member who may not make the call
   proposes it, and the operator approves it where the card's
   weight is approved (a strong card in a terminal, in footplate,
   or in the hosted web after a fresh sign-in):
   ```sh
   yardr bead ask <bead> "Upload the Worker?" --run "call cloudflare worker_upload account=<account id> name=<worker> script=<file>"
   yardr bead ask <bead> "Put the Worker's secret?" --run "call cloudflare worker_secret account=<account id> name=<worker> secret=<SECRET_NAME> value=<secret file>"
   yardr bead ask <bead> "Send mail to the Worker?" --run "call cloudflare email_rule zone=<zone id> address=<address> worker=<worker>"
   ```
   The upload card shows the file's SHA-256 digest; those bytes
   are what is sent, and a changed file is a new card. The secret
   card shows the secret's name and the file's name alone; the
   yard reads the value from that file when the approved call is
   made, and the value stands in no card, note or record. To send
   an address to a Worker where a rule for it stands, list the
   rules first (`email_rules`) and edit the standing rule rather
   than making a second: `--run "call cloudflare email_rule_edit
   zone=<zone id> rule=<rule id> address=<address> worker=<worker>"`.
   The operator may also make each call at a terminal without a
   card: `yardr call cloudflare <call> …`.

What is not there, on purpose: deleting a Worker, a secret or a
rule; other Cloudflare products (DNS, R2, Pages); wrangler itself;
and no credential but the one token.