Skip to content

AGENTS.md

227 lines · 7.2 KB · Markdown
Raw

@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 aliasSecret name (user scope)Known Cloudflare account idDefault
defaultcloudflareApiTokennone (multi-account)Yes
kodycloudflareApiTokenKodyAccountKody (accounts() → accountId)No
pagescloudflarePagesApiTokenKCD / 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

ExportImport
overviewkody:@kentcdodds/cloudflare
overview (alias)kody:@kentcdodds/cloudflare/overview
accountskody:@kentcdodds/cloudflare/accounts
verifykody:@kentcdodds/cloudflare/verify
api-v4kody:@kentcdodds/cloudflare/api-v4
api-v4-listkody:@kentcdodds/cloudflare/api-v4-list
publish-statickody:@kentcdodds/cloudflare/publish-static
analyticskody:@kentcdodds/cloudflare/analytics
docskody:@kentcdodds/cloudflare/docs
observability-logskody:@kentcdodds/cloudflare/observability-logs
rulesetskody:@kentcdodds/cloudflare/rulesets
r2-put-objectkody:@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. 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):

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; 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.