# @kentcdodds/cloudflare — agent notes Human setup and intent live in [`README.md`](./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) ```js import verifyApiToken from 'kody:@kentcdodds/cloudflare/verify' export default async function main() { return await verifyApiToken({ account: 'pages' }) // => { ok: true, kind: 'user', accountId: '', 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 ```js 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': '

Hello

', '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: ```js 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. `createIfMissing` creates with `production_branch` (default `main`). Deploying to a Git-connected project fails with a clear error — pick another project name. - `_headers` / `_redirects` are 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: false` returns as soon as Cloudflare queues the deployment (`queued:idle`); default waits until `deploy: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): ```js 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: ```js 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: ```js 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: ```js 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`): ```js 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): ```js 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; `default` does not — pass `accountId` or read `verify().accessibleAccounts`. - After verify: `401` = rejected (or wrong endpoint); `403` = valid but under-permissioned; `400`/`404` on a mutating call against a bad/missing target = authorized for that operation. - Prefer `analytics` when Log Explorer returns missing entitlement. - `observability-logs` defaults to `workers_trace_events`, last hour, limit 50; pass `allTime: true` to omit the time window. - `api-v4` supports `body` (JSON), `form` (multipart), or `rawBody`. Prefer `publish-static` for Pages and `r2-put-object` for R2 PUTs from a URL. - Never paste Cloudflare token values into chat or logs.