← Public packages
@kentcdodds/cloudflare
Cloudflare helpers: API v4, static site publishing to Pages, analytics, WAF rules, logs, and docs.
src/accounts-resolve.ts
67 lines · 2.6 KB · TypeScriptimport cloudflareApiV4List from './api-v4-list.ts'
import {
DEFAULT_CLOUDFLARE_ACCOUNT,
pickAuthOptions,
resolveApiTokenSecretName,
resolveKnownAccountId,
type CloudflareAuthOptions,
} from './auth.ts'
export type AccessibleAccount = { id: string; name: string }
/** List accounts the token can see via GET /accounts (empty for Pages-only tokens). */
export async function listAccessibleAccounts(
auth: CloudflareAuthOptions,
): Promise<{ ok: boolean; accounts: AccessibleAccount[]; httpStatus: number | null }> {
const res = await cloudflareApiV4List<{ id?: string; name?: string }>({
...pickAuthOptions(auth),
path: '/client/v4/accounts',
maxPages: 10,
})
return {
ok: res.ok,
httpStatus: res.httpStatus,
accounts: res.items
.filter((account) => typeof account.id === 'string')
.map((account) => ({ id: account.id as string, name: String(account.name ?? '') })),
}
}
/**
* Resolve the Cloudflare account id for an account-scoped call: explicit
* `accountId`, then the alias's baked-in id, then GET /accounts when the token
* sees exactly one account. Throws with the options when ambiguous.
*/
export async function resolveAccountId(
params: CloudflareAuthOptions & { accountId?: string },
): Promise<{ accountId: string; source: 'param' | 'alias' | 'discovered' }> {
if (typeof params.accountId === 'string' && params.accountId.trim()) {
const accountId = params.accountId.trim()
if (!/^[a-f0-9]{32}$/.test(accountId)) {
throw new Error('accountId must be a 32-character hex Cloudflare account id.')
}
return { accountId, source: 'param' }
}
if (!params.apiTokenSecret) {
const known = resolveKnownAccountId({ account: params.account })
if (known) return { accountId: known, source: 'alias' }
}
const listed = await listAccessibleAccounts(params)
if (listed.accounts.length === 1) {
return { accountId: listed.accounts[0].id, source: 'discovered' }
}
const secret = resolveApiTokenSecretName(pickAuthOptions(params))
const alias = params.apiTokenSecret ? `secret ${secret}` : `account "${params.account ?? DEFAULT_CLOUDFLARE_ACCOUNT}"`
if (listed.accounts.length > 1) {
throw new Error(
`accountId is required: ${alias} can access ${listed.accounts.length} accounts — ` +
listed.accounts.map((account) => `${account.name} (${account.id})`).join(', ') +
'. Pass accountId.',
)
}
throw new Error(
`accountId is required: ${alias} cannot list accounts (GET /accounts returned none). ` +
'Copy the account id from the Cloudflare dashboard URL (dash.cloudflare.com/<accountId>/…) or ' +
'Workers & Pages → Account details, and pass accountId.',
)
}