export type CloudflareAccount = 'default' | 'kody' | 'pages' /** * Known Cloudflare account ids this package can default for an alias. * `null` means the alias does not pin an account — callers pass `accountId` * (or discover it via GET /accounts with a token that can list them). */ export type CloudflareAccountInfo = { account: CloudflareAccount label: string default: boolean secretName: string /** * Cloudflare account id when the alias maps to one known account. * - `kody` → Kody production account * - `pages` → Kent's personal "KCD" account (Pages token is scoped there) * - `default` → `null` (token covers multiple accounts; pass `accountId`) */ accountId: string | null /** Friendly Cloudflare account name when known. */ accountName: string | null useWhen: string avoidWhen: string /** Capabilities this alias's token typically has. */ can: string[] /** Things this alias's token typically cannot do. */ cannot: string[] } export type CloudflareAuthOptions = { /** * Credential alias mapped to a known Kody secret. Defaults to `default` * (`cloudflareApiToken`). */ account?: CloudflareAccount /** * Explicit Kody secret name when no alias covers the token. Never pass a raw * token value — only the secret's name. Mutually exclusive with `account`. */ apiTokenSecret?: string } export const DEFAULT_CLOUDFLARE_ACCOUNT: CloudflareAccount = 'default' /** Kody production Cloudflare account id (`account: 'kody'`). */ export const KODY_CLOUDFLARE_ACCOUNT_ID = 'a99ee2e72728dd52902ef288b7b1447d' /** * Kent's personal "KCD" Cloudflare account id. The `pages` alias token is * scoped to this account (GET /accounts returns empty for that token, so the * id is baked into the alias metadata). */ export const KCD_CLOUDFLARE_ACCOUNT_ID = 'a41d50ecaf0ae0f86dd1824ef6729cb2' const cloudflareAccounts = [ { account: 'default', label: 'Default Cloudflare API token', default: true, secretName: 'cloudflareApiToken', accountId: null, accountName: null, useWhen: 'Routine Cloudflare API work against accounts and zones covered by the primary (user-scoped) token. Covers multiple accounts — pass accountId for account-scoped resources.', avoidWhen: 'The primary token is not authorized for the target account (for example the Kody Cloudflare account), or you need the dedicated Pages token.', can: [ 'List accounts the token can see (GET /accounts)', 'Zones, Workers, D1, R2, KV, Queues, analytics, Log Explorer, WAF rulesets on covered accounts', 'Pages projects on accounts the token can access (often including KCD)', ], cannot: [ 'Guess a single default accountId — this token spans multiple accounts', 'Kody-account resources the kody alias owns exclusively', ], }, { account: 'kody', label: 'Kody Cloudflare account token', default: false, secretName: 'cloudflareApiTokenKodyAccount', accountId: KODY_CLOUDFLARE_ACCOUNT_ID, accountName: 'Kody', useWhen: 'Calls against the Kody Cloudflare account (Workers, D1, R2, Log Explorer on that account). Typically an account-scoped token — verify with kody:@kentcdodds/cloudflare/verify ({ account: "kody" }), not /user/tokens/verify alone.', avoidWhen: 'Zones and resources that the default token already covers, or Pages on the KCD account.', can: [ 'Workers, D1, R2, KV, Queues, Log Explorer on the Kody account', 'GET /accounts for the Kody account only', 'Account-scoped token verify at /accounts/{id}/tokens/verify', ], cannot: [ 'KCD (personal) account Pages / zones', 'User-scoped /user/tokens/verify (expect 401 — use ./verify instead)', ], }, { account: 'pages', label: 'Cloudflare Pages API token', default: false, secretName: 'cloudflarePagesApiToken', accountId: KCD_CLOUDFLARE_ACCOUNT_ID, accountName: 'KCD', useWhen: 'Cloudflare Pages Direct Upload and Pages project management on the KCD (personal) account. Prefer publishStatic over hand-rolling the upload JWT + multipart flow.', avoidWhen: 'General Workers/D1/R2/analytics/WAF work (use default or kody). DNS edits for custom domains (Pages tokens usually lack Zone DNS edit).', can: [ 'List / create / deploy Pages projects on the KCD account', 'Pages Direct Upload (upload JWT + multipart deployment) — use ./publish-static', ], cannot: [ 'List accounts via GET /accounts (returns empty even when valid — use the baked-in accountId)', 'R2 / most non-Pages resources', 'Edit zone DNS for custom Pages domains (document the hostname; configure DNS separately)', 'Kody-account Pages (there usually are none; use the KCD accountId)', ], }, ] as const satisfies readonly CloudflareAccountInfo[] const SECRET_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/ /** * Return the supported Cloudflare account aliases and selection guidance. */ export function accounts(): readonly CloudflareAccountInfo[] { return cloudflareAccounts } /** * Return documentation for one Cloudflare account alias. */ export function getAccountInfo( account: CloudflareAccount = DEFAULT_CLOUDFLARE_ACCOUNT, ): CloudflareAccountInfo { const info = cloudflareAccounts.find((candidate) => candidate.account === account) if (!info) { throw new Error('Unsupported Cloudflare account: ' + String(account)) } return info } /** * Resolve a Cloudflare account id from an explicit value or a known alias. * Returns `null` when neither is available (caller must pass `accountId`). */ export function resolveKnownAccountId(options: { account?: CloudflareAccount accountId?: string }): string | null { if (typeof options.accountId === 'string' && options.accountId.trim()) { return options.accountId.trim() } const account = options.account ?? DEFAULT_CLOUDFLARE_ACCOUNT return getAccountInfo(account).accountId } const KNOWN_API_TOKEN_SECRETS = [ 'cloudflareApiToken', 'cloudflareApiTokenKodyAccount', 'cloudflarePagesApiToken', ] as const type KnownApiTokenSecret = (typeof KNOWN_API_TOKEN_SECRETS)[number] /** * Resolve the Kody secret name for an optional account alias or explicit secret. * Only the three declared Cloudflare token slots are stampable. */ export function resolveApiTokenSecretName(options: CloudflareAuthOptions = {}): KnownApiTokenSecret { const hasExplicit = options.apiTokenSecret !== undefined && options.apiTokenSecret !== null && String(options.apiTokenSecret).trim() !== '' if (hasExplicit && options.account !== undefined) { throw new Error('Pass either account or apiTokenSecret, not both.') } if (hasExplicit) { const name = String(options.apiTokenSecret).trim() if (!SECRET_NAME.test(name)) { throw new Error( 'apiTokenSecret must be a simple Kody secret name (letters, numbers, underscore).', ) } switch (name) { case 'cloudflareApiToken': case 'cloudflareApiTokenKodyAccount': case 'cloudflarePagesApiToken': return name default: throw new Error( 'apiTokenSecret must be one of: cloudflareApiToken, cloudflareApiTokenKodyAccount, cloudflarePagesApiToken.', ) } } const account = options.account ?? DEFAULT_CLOUDFLARE_ACCOUNT switch (account) { case 'default': return 'cloudflareApiToken' case 'kody': return 'cloudflareApiTokenKodyAccount' case 'pages': return 'cloudflarePagesApiToken' default: { const exhaustive: never = account throw new Error('Unsupported Cloudflare account: ' + String(exhaustive)) } } } /** * Build the Authorization header value using a `{{secret:name}}` placeholder. * Never accepts or returns a raw token value. */ export function getAuthorizationHeader(options: CloudflareAuthOptions = {}): string { const secretName = resolveApiTokenSecretName(options) switch (secretName) { case 'cloudflareApiToken': return 'Bearer {{secret:cloudflareApiToken}}' case 'cloudflareApiTokenKodyAccount': return 'Bearer {{secret:cloudflareApiTokenKodyAccount}}' case 'cloudflarePagesApiToken': return 'Bearer {{secret:cloudflarePagesApiToken}}' default: { const exhaustive: never = secretName throw new Error('Unsupported Cloudflare API token secret: ' + String(exhaustive)) } } } /** * Common Cloudflare API headers including the selected token placeholder. */ export function cloudflareAuthHeaders( options: CloudflareAuthOptions = {}, ): Record { return { Accept: 'application/json', Authorization: getAuthorizationHeader(options), 'User-Agent': 'kody-cloudflare/1.0', } } /** * Pick auth-related fields from a wider params object. */ export function pickAuthOptions(params: CloudflareAuthOptions = {}): CloudflareAuthOptions { const out: CloudflareAuthOptions = {} if (params.account !== undefined) out.account = params.account if (params.apiTokenSecret !== undefined) out.apiTokenSecret = params.apiTokenSecret return out }