01 / apply it
yardr pack apply cloudflareyardr 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.