@kentcdodds/cloudflare
Cloudflare helpers: API v4, static site publishing to Pages, analytics, WAF rules, logs, and docs.
AGENTS.md
227 lines · 7.2 KB · Markdown@kentcdodds/cloudflare — agent notes
Human setup and intent live in README.md. This file is for
agents: imports, smoke / dryRun execute snippets, and edge cases. Secrets by
name only — never paste token values. Do not disable live webhooks or jobs.
Secrets
| Account alias | Secret name (user scope) | Known Cloudflare account id | Default |
|---|---|---|---|
default | cloudflareApiToken | none (multi-account) | Yes |
kody | cloudflareApiTokenKodyAccount | Kody (accounts() → accountId) | No |
pages | cloudflarePagesApiToken | KCD / personal (accounts() → accountId) | No |
Host: api.cloudflare.com. For an unlisted token, pass apiTokenSecret with the
secret name only. Never pass both account and apiTokenSecret.
Call accounts() (no secret) to see each alias's can / cannot lists and
baked-in accountId. Prefer pages for Pages Direct Upload on KCD.
Import paths
| Export | Import |
|---|---|
| overview | kody:@kentcdodds/cloudflare |
| overview (alias) | kody:@kentcdodds/cloudflare/overview |
| accounts | kody:@kentcdodds/cloudflare/accounts |
| verify | kody:@kentcdodds/cloudflare/verify |
| api-v4 | kody:@kentcdodds/cloudflare/api-v4 |
| api-v4-list | kody:@kentcdodds/cloudflare/api-v4-list |
| publish-static | kody:@kentcdodds/cloudflare/publish-static |
| analytics | kody:@kentcdodds/cloudflare/analytics |
| docs | kody:@kentcdodds/cloudflare/docs |
| observability-logs | kody:@kentcdodds/cloudflare/observability-logs |
| rulesets | kody:@kentcdodds/cloudflare/rulesets |
| r2-put-object | kody:@kentcdodds/cloudflare/r2-put-object |
Prefer static kody:@kentcdodds/cloudflare/... imports from execute. Do not
lead with packages.invoke.
Smoke test (read-only)
import verifyApiToken from 'kody:@kentcdodds/cloudflare/verify'
export default async function main() {
return await verifyApiToken({ account: 'pages' })
// => { ok: true, kind: 'user', accountId: '<KCD>', accessibleAccounts: [], ... }
}Also useful: accounts (no secret) or docs({ path: '/pages/' }) (no secret).
Never conclude a secret is broken from /user/tokens/verify alone. Account
tokens often 401 there while still valid — use ./verify. The pages token
returns accessibleAccounts: [] even when valid; use its baked-in accountId.
publish-static
import publishStatic from 'kody:@kentcdodds/cloudflare/publish-static'
export default async function main() {
return await publishStatic({
account: 'pages',
target: { kind: 'pages', project: 'my-demo-site' },
files: {
'index.html': '<!doctype html><h1>Hello</h1>',
'styles.css': 'h1 { color: rebeccapurple }',
},
createIfMissing: true,
scan: { forbid: ['INTERNAL_ONLY'] },
dryRun: true, // flip to false for a real deploy
})
}From a public zip / tar / tar.gz of the built site:
import publishStatic from 'kody:@kentcdodds/cloudflare/publish-static'
export default async function main() {
return await publishStatic({
account: 'pages',
target: { kind: 'pages', project: 'my-demo-site' },
sourceUrl: 'https://example.com/site.zip',
// sourceRoot: 'dist', // or '*/dist' for a GitHub release tarball
spa: true, // /* → /index.html 200
noindex: true, // X-Robots-Tag: noindex
headers: { '/*': { 'X-Frame-Options': 'DENY' } },
createIfMissing: true,
wait: true, // default; poll until deploy:success
})
}Notes:
- Direct Upload projects are not Git-connected.
createIfMissingcreates withproduction_branch(defaultmain). Deploying to a Git-connected project fails with a clear error — pick another project name. _headers/_redirectsare deployment form fields (not uploaded assets).- The upload JWT is fetched and refreshed inside the helper — never put it in caller code.
- Every public publish runs
scan(built-in credential patterns +scan.forbid). A match refuses the upload. Dry runs still report findings. wait: falsereturns as soon as Cloudflare queues the deployment (queued:idle); default waits untildeploy:success.- Custom domains / Access / DNS / deployment rollback are not handled here.
- Workers Assets and public R2 static hosting are not implemented — use
target.kind: 'pages'only.
dryRun snippets
Log Explorer SQL preview (no query execution):
import searchLogs from 'kody:@kentcdodds/cloudflare/observability-logs'
export default async function main() {
return await searchLogs({
accountId: 'your-account-id',
scriptName: 'my-worker',
outcome: 'exception',
last: '30m',
limit: 25,
dryRun: true,
})
}Rulesets default to dry-run. Pass apply: true (or dryRun: false) only after
reviewing the returned entrypoint body:
import rulesets from 'kody:@kentcdodds/cloudflare/rulesets'
export default async function main() {
return await rulesets({
zoneName: 'example.com',
/* rule params… */
dryRun: true,
})
}Common snippets
API v4 JSON:
import apiV4 from 'kody:@kentcdodds/cloudflare/api-v4'
export default async function main() {
return await apiV4({
account: 'kody',
method: 'POST',
path: '/client/v4/accounts/ACCOUNT_ID/d1/database/DB_ID/query',
body: { sql: 'SELECT 1 AS ok' },
})
}API v4 multipart (form) — prefer publish-static for Pages deploys:
import apiV4 from 'kody:@kentcdodds/cloudflare/api-v4'
export default async function main() {
return await apiV4({
account: 'pages',
method: 'POST',
path: '/client/v4/accounts/ACCOUNT_ID/pages/projects/my-site/deployments',
form: {
manifest: JSON.stringify({ '/index.html': 'abc…' }),
_headers: { filename: '_headers', content: '/*\n X-Robots-Tag: noindex\n' },
},
})
}Paginated list (Pages projects reject per_page > 10 — omit perPage):
import listAll from 'kody:@kentcdodds/cloudflare/api-v4-list'
import accounts from 'kody:@kentcdodds/cloudflare/accounts'
export default async function main() {
const pages = accounts().find((a) => a.account === 'pages')
return await listAll({
account: 'pages',
path: `/client/v4/accounts/${pages.accountId}/pages/projects`,
})
}R2 PUT (prefer sourceUrl for large files):
import r2PutObject from 'kody:@kentcdodds/cloudflare/r2-put-object'
export default async function main() {
return await r2PutObject({
accountId: 'ACCOUNT_ID',
bucket: 'my-bucket',
key: 'smoke/hello.txt',
contentType: 'text/plain',
bytesBase64: btoa('hello'),
confirm: true,
})
}Edge cases
account: 'kody'/'pages'supply a default account id;defaultdoes not — passaccountIdor readverify().accessibleAccounts.- After verify:
401= rejected (or wrong endpoint);403= valid but under-permissioned;400/404on a mutating call against a bad/missing target = authorized for that operation. - Prefer
analyticswhen Log Explorer returns missing entitlement. observability-logsdefaults toworkers_trace_events, last hour, limit 50; passallTime: trueto omit the time window.api-v4supportsbody(JSON),form(multipart), orrawBody. Preferpublish-staticfor Pages andr2-put-objectfor R2 PUTs from a URL.- Never paste Cloudflare token values into chat or logs.