hcengineering/platform · error · TokenError
Token revoked
Error message
Token revoked
What it means
verifyToken() rejects an otherwise valid, unexpired token because its apiTokenId (in decoded.extra) is reported as revoked by the registered apiTokenRevocationChecker. Revocation is checked with a short TTL cache; the account service is the sole authority, so a revoked token is refused even if its signature and expiry are still fine. Callers such as the transactor REST API and blob access surface this as a TokenError.
Source
Thrown at foundations/core/packages/token/src/token.ts:227
}
}
/**
* Decodes and fully validates a token: signature (via {@link decodeToken}),
* expiry, and — for revokable API tokens — revocation. Reuse this instead of
* `decodeToken` anywhere expired or revoked tokens must be rejected (transactor
* REST API, blob access, etc.) so the policy lives in one place.
* @public
*/
export async function verifyToken (token: string, secret?: string): Promise<Token> {
const decoded = decodeToken(token, true, secret)
if (isTokenExpired(decoded)) {
throw new TokenError('Token expired')
}
const apiTokenId = decoded.extra?.apiTokenId
if (apiTokenId !== undefined && apiTokenRevocationChecker !== undefined) {
if (await isApiTokenRevoked(apiTokenId, decoded, token, Date.now())) {
throw new TokenError('Token revoked')
}
}
return decoded
}
View on GitHub (pinned to 63e28dc964)
Solutions
- Obtain a fresh, non-revoked API token from the account service and replace the configured token.
- Check the token's apiTokenId against the account service revocation list to confirm why it was revoked.
- If the revocation was unintended, re-issue/restore the token in the account service.
- If revocation should not apply, note that cache entries within the TTL are trusted — wait out the TTL after un-revoking, then retry.
Example fix
// before
const token = process.env.OLD_API_TOKEN // revoked in account service
await verifyToken(token) // throws TokenError('Token revoked')
// after
const token = process.env.API_TOKEN // freshly issued, unrevoked
await verifyToken(token) Defensive patterns
Strategy: try-catch
Validate before calling
import { decodeToken, isTokenExpired } from '<token-package>' // pseudo
const decoded = decodeToken(token, true, secret)
if (isTokenExpired(decoded)) throw new Error('expired first')
const apiTokenId = decoded.extra?.apiTokenId
if (apiTokenId !== undefined && !apiTokenId) throw new Error('malformed apiTokenId') Type guard
function hasApiTokenId(t: unknown): t is Token & { extra: { apiTokenId: string } } {
return typeof t === 'object' && t !== null &&
typeof (t as any).extra?.apiTokenId === 'string'
} Try / catch
try {
const decoded = await verifyToken(token)
// use decoded
} catch (e) {
if (e instanceof TokenError && e.message === 'Token revoked') {
// prompt re-auth / issue new API token
} else throw e
} Prevention
- Refresh API tokens on a schedule rather than caching one indefinitely.
- Treat any 4xx auth response as a prompt to re-fetch the token from the account service.
- Monitor revocation events server-side and push token invalidation to clients proactively.
- Never hard-code API tokens in config files; source them from a secret store you can rotate.
When it happens
Trigger: verifyToken(token) is called; decodeToken succeeds, the token is not expired, decoded.extra.apiTokenId is defined, a revocation checker is registered (setApiTokenRevocationChecker), and the checker (or a fresh cache entry, i.e. outside REVOCATION_CACHE_TTL_MS) reports revoked=true.
Common situations: A user deleted or rotated their API token in the account service while an old token string is still configured in a client; an admin revoked a leaked token; a personal access token was removed after an offboarding.
Related errors
- Invalid hook token
- platform.status.Unauthorized
- platform.status.Forbidden
- Workspace or account not found in token
- Unauthorized
AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29).
Data as JSON: /api/errors/78e74c9e0a46d8ae.
Report an issue: GitHub.