import cloudflareApiV4 from './api-v4.ts'
import {
DEFAULT_CLOUDFLARE_ACCOUNT,
getAccountInfo,
pickAuthOptions,
resolveApiTokenSecretName,
type CloudflareAuthOptions,
} from './auth.ts'
/**
* Production Kody Cloudflare account id. Used as the default target for
* account-scoped token verification when `account: 'kody'`.
*/
export const KODY_CLOUDFLARE_ACCOUNT_ID = 'a99ee2e72728dd52902ef288b7b1447d'
export type VerifyApiTokenInput = CloudflareAuthOptions & {
/**
* Cloudflare account id for account-scoped token verification
* (`GET /accounts/{account_id}/tokens/verify`). Required when the token is
* account-scoped and `account` is not `kody` (which defaults to
* {@link KODY_CLOUDFLARE_ACCOUNT_ID}).
*/
accountId?: string
}
export type VerifyApiTokenResult = {
/** True when Cloudflare reported the token as usable. */
ok: boolean
/** Which verify endpoint succeeded, or null when neither did. */
kind: 'user' | 'account' | null
/** Cloudflare token id when returned by a successful verify call. */
tokenId: string | null
/** Cloudflare token status (`active`, etc.) when returned. */
status: string | null
/** Account id used for the account-scoped verify attempt, if any. */
accountId: string | null
/** Kody secret name that was used (never the token value). */
secretName: string
/** HTTP status from the successful verify, or the last attempt. */
httpStatus: number | null
/** Compact error list from failed attempts. */
errors: Array<{ code?: number; message?: string }>
/**
* Agent-facing guidance. Especially important when `/user/tokens/verify`
* returns 401 for a valid account-scoped token.
*/
guidance: string
}
type CloudflareError = { code?: number; message?: string }
function asErrors(errors: unknown): Array<CloudflareError> {
if (!Array.isArray(errors)) return []
return errors.map((error) => {
if (!error || typeof error !== 'object') return {}
const record = error as CloudflareError
return {
code: typeof record.code === 'number' ? record.code : undefined,
message: typeof record.message === 'string' ? record.message : undefined,
}
})
}
function resolveAccountIdForVerify(params: VerifyApiTokenInput): string | null {
if (typeof params.accountId === 'string' && params.accountId.trim()) {
return params.accountId.trim()
}
const account = params.account ?? DEFAULT_CLOUDFLARE_ACCOUNT
if (account === 'kody') return KODY_CLOUDFLARE_ACCOUNT_ID
return null
}
/**
* Verify a Cloudflare API token without misreading account-scoped tokens.
*
* **Do not use `/user/tokens/verify` alone.** Account API tokens (Manage
* Account → API Tokens) often return `401 Invalid API Token` on the user
* endpoint even when fully valid. Use:
*
* - User tokens (My Profile → API Tokens): `GET /user/tokens/verify`
* - Account tokens: `GET /accounts/{account_id}/tokens/verify`
*
* This helper tries the user endpoint first, then the account endpoint when an
* `accountId` is known (`account: 'kody'` defaults to the Kody account id).
*
* A successful verify only means the token is alive. Permission checks still
* need resource probes: `403` = lacks permission; `400`/`404` on a mutating
* call against a bad/missing target = authorized for that operation.
*
* @param params.account - Token alias (`default`, `kody`, `pages`).
* @param params.apiTokenSecret - Explicit Kody secret name when no alias fits.
* @param params.accountId - Cloudflare account id for account-scoped verify.
* @returns Compact verification result (never includes the token value).
* @example
* import verifyApiToken from 'kody:@kentcdodds/cloudflare/verify'
* const result = await verifyApiToken({ account: 'kody' })
* // => { ok: true, kind: 'account', status: 'active', ... }
*/
export default async function verifyApiToken(
params: VerifyApiTokenInput = {},
): Promise<VerifyApiTokenResult> {
const auth = pickAuthOptions(params)
const secretName = resolveApiTokenSecretName(auth)
const accountAlias = params.account ?? DEFAULT_CLOUDFLARE_ACCOUNT
const accountId = resolveAccountIdForVerify(params)
const userVerify = await cloudflareApiV4({
...auth,
path: '/client/v4/user/tokens/verify',
})
if (userVerify.success) {
const result =
userVerify.result && typeof userVerify.result === 'object'
? (userVerify.result as { id?: string; status?: string })
: {}
return {
ok: true,
kind: 'user',
tokenId: typeof result.id === 'string' ? result.id : null,
status: typeof result.status === 'string' ? result.status : null,
accountId: null,
secretName,
httpStatus: userVerify.httpStatus,
errors: [],
guidance:
`Token for secret "${secretName}" verified via /user/tokens/verify ` +
`(user-scoped / profile token). Alias: ${accountAlias}.`,
}
}
if (accountId) {
const accountVerify = await cloudflareApiV4({
...auth,
path: `/client/v4/accounts/${accountId}/tokens/verify`,
})
if (accountVerify.success) {
const result =
accountVerify.result && typeof accountVerify.result === 'object'
? (accountVerify.result as { id?: string; status?: string })
: {}
return {
ok: true,
kind: 'account',
tokenId: typeof result.id === 'string' ? result.id : null,
status: typeof result.status === 'string' ? result.status : null,
accountId,
secretName,
httpStatus: accountVerify.httpStatus,
errors: [],
guidance:
`Token for secret "${secretName}" verified via ` +
`/accounts/${accountId}/tokens/verify (account-scoped token). ` +
`A prior /user/tokens/verify 401 is expected for account tokens and ` +
`must not be treated as “token invalid.”`,
}
}
return {
ok: false,
kind: null,
tokenId: null,
status: null,
accountId,
secretName,
httpStatus: accountVerify.httpStatus,
errors: [
...asErrors(userVerify.errors),
...asErrors(accountVerify.errors),
],
guidance:
`Both verify endpoints failed for secret "${secretName}". ` +
`User verify HTTP ${String(userVerify.httpStatus)}; account verify ` +
`HTTP ${String(accountVerify.httpStatus)} against account ${accountId}. ` +
`The stored secret value is likely wrong, revoked, or expired — not merely under-scoped.`,
}
}
const aliasHint =
accountAlias === 'kody'
? ''
: ` For the Kody account token, call verifyApiToken({ account: 'kody' }). ` +
`For other account-scoped tokens, pass accountId.`
return {
ok: false,
kind: null,
tokenId: null,
status: null,
accountId: null,
secretName,
httpStatus: userVerify.httpStatus,
errors: asErrors(userVerify.errors),
guidance:
`/user/tokens/verify failed for secret "${secretName}" ` +
`(HTTP ${String(userVerify.httpStatus)}). This is NOT proof the token is ` +
`invalid — account-scoped tokens commonly 401 here. Retry with accountId ` +
`(GET /accounts/{account_id}/tokens/verify).${aliasHint} ` +
`Alias info: ${getAccountInfo(accountAlias).label}.`,
}
}