{"record":{"id":"3714f9d8496278f1","repo":"hcengineering/platform","slug":"token-revocation-could-not-be-verified","errorCode":null,"errorMessage":"Token revocation could not be verified","messagePattern":"Token revocation could not be verified","errorType":"exception","errorClass":"TokenError","httpStatus":null,"severity":"error","filePath":"foundations/core/packages/token/src/token.ts","lineNumber":208,"sourceCode":"}\n\nasync function isApiTokenRevoked (apiTokenId: string, token: Token, raw: string, now: number): Promise<boolean> {\n  const cached = revocationCache.get(apiTokenId)\n  if (cached !== undefined && now - cached.checkedAt <= REVOCATION_CACHE_TTL_MS) {\n    return cached.revoked\n  }\n\n  try {\n    const revoked = await (apiTokenRevocationChecker as ApiTokenRevocationChecker)(apiTokenId, token, raw)\n    cacheRevocation(apiTokenId, revoked, now)\n    return revoked\n  } catch {\n    // The account is the only authority on revocation. If it cannot be reached we\n    // do not know whether this token still stands, so refuse it rather than let a\n    // revoked token survive by making the account unreachable. A verdict from\n    // within the TTL is still trusted, which keeps brief outages from cutting off\n    // healthy tokens mid-flight.\n    throw new TokenError('Token revocation could not be verified')\n  }\n}\n\n/**\n * Decodes and fully validates a token: signature (via {@link decodeToken}),\n * expiry, and — for revokable API tokens — revocation. Reuse this instead of\n * `decodeToken` anywhere expired or revoked tokens must be rejected (transactor\n * REST API, blob access, etc.) so the policy lives in one place.\n * @public\n */\nexport async function verifyToken (token: string, secret?: string): Promise<Token> {\n  const decoded = decodeToken(token, true, secret)\n  if (isTokenExpired(decoded)) {\n    throw new TokenError('Token expired')\n  }\n  const apiTokenId = decoded.extra?.apiTokenId\n  if (apiTokenId !== undefined && apiTokenRevocationChecker !== undefined) {\n    if (await isApiTokenRevoked(apiTokenId, decoded, token, Date.now())) {","sourceCodeStart":190,"sourceCodeEnd":226,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/foundations/core/packages/token/src/token.ts#L190-L226","documentation":"For revokable API tokens, isApiTokenRevoked asks the account service whether the token was revoked. If the account service cannot be reached, revocation status is unknown, and the code deliberately refuses the token (fail-closed) rather than letting a possibly-revoked token pass, while still trusting cached verdicts within the TTL.","triggerScenarios":"verifyToken on a token with extra.apiTokenId when the account/revocation service call throws — network outage, DNS failure, timeout, or service crash.","commonSituations":"Partial outages where auth works but the account service is down; network partitions; misconfigured account service endpoint; brief blips during deploys (mitigated by the TTL cache).","solutions":["Restore connectivity to the account service and retry verification","Check the account service health/endpoint configuration","Increase or rely on the revocation-verdict TTL so brief outages don't break healthy tokens","As a caller, catch TokenError and surface 'try again shortly' rather than treating it as a bad credential"],"exampleFix":"// before\nconst token = await verifyToken(raw) // throws during account-service blip\n// after\ntry {\n  const token = await verifyToken(raw)\n} catch (e) {\n  if (e.message === 'Token revocation could not be verified') return retryLater()\n  throw e\n}","handlingStrategy":"try-catch","validationCode":"// No caller-side check can confirm revocation; precondition is account-service reachability\nawait healthCheck(accountServiceUrl) // fail fast if revocation authority is unreachable","typeGuard":null,"tryCatchPattern":"try {\n  const t = await verifyToken(token)\n} catch (e) {\n  if (e instanceof TokenError && e.message === 'Token revocation could not be verified') {\n    // fail-closed: treat as temporary unavailability, not bad credentials\n    return res.status(503).send('auth backend unavailable, retry shortly')\n  }\n  throw e\n}","preventionTips":["Return 503 with retry, not 401, when this specific message appears","Monitor account-service health; alert on rising revocation-check failures","Keep the revocation-verdict TTL long enough to ride out brief outages","Include retry/backoff around verifyToken in background consumers"],"tags":["network","token","revocation","fail-closed"],"backgroundTag":"revocation-check-unavailable","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}