import 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//…) or ' + 'Workers & Pages → Account details, and pass accountId.', ) }