← Public packages
@kentcdodds/cloudflare
Cloudflare helpers: API v4, static site publishing to Pages, analytics, WAF rules, logs, and docs.
src/auth.ts
252 lines · 8.6 KB · TypeScriptexport 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<string, string> {
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
}