On this page
Parse, expire, and write OAuth token entries in ~/.claudewheel/tokens.json.
#claudewheel.tokens
#claudewheel.tokens
Parse, expire, and write OAuth token entries in ~/.claudewheel/tokens.json.
#TokenExpiryDisposition
How a token's expiry is recorded when writing it to tokens.json.
The caller MUST choose one explicitly -- there is no default -- so token lifetime is never silently fabricated.
TTL: a claude setup-token, genuinely valid forTOKEN_TTL_DAYS.
The entry gets created (today) and expires_at (created + TTL).
UNKNOWN: an externally-issued token whose expiry we cannot know.
The entry gets created (today) and the expiry_unknown marker, and NO expires_at -- expiry is reported as unknown, never assumed.
#parse_entry
def parse_entry(entry: object) -> str | NoneExtract the token string from a tokens.json entry.
Supports both formats: a bare string, or a dict like {"token": ..., "created": ..., "expires_at": ...}. Returns None if the entry is empty, absent, or unrecognized.
#TokenExpiry
Computed token lifetime: creation date, expiry date, days remaining.
remaining_days is None only for entries marked with an unknown expiry disposition -- a distinct, honest "we don't know" that consumers must handle separately from the "assume fresh" fallback (which reports a concrete TOKEN_TTL_DAYS).
#compute_expiry
def compute_expiry(entry: object, tokens_mtime: float, today: date | None=None) -> TokenExpiryCompute a token entry's creation date, expiry date, and remaining days.
Precedence: an explicit unknown-expiry marker yields (None, None, None); else explicit "expires_at" ISO date; else "created" + TOKEN_TTL_DAYS; else (legacy bare-string entry) the tokens.json file mtime + TOKEN_TTL_DAYS. Unparseable or absent dict fields yield (None, None, TOKEN_TTL_DAYS), matching the historical health-check behavior of assuming a fresh token.
#_read_tokens_for_write
def _read_tokens_for_write(path: Path) -> dict[str, Any]Read a tokens.json for a write operation. Missing -> {}; corrupt -> OSError.
Preserves the historical write-path contract: a corrupt file is a hard OSError so callers never silently clobber it. Shared by TokenStore.add and TokenStore.set_tier so the message and behavior stay identical regardless of which path is targeted.
#_write_token
def _write_token(path: Path, name: str, token: str, *, expiry: TokenExpiryDisposition, tier: str | None=None, subscription: str | None=None) -> NoneAdd/update a token entry in the tokens.json at path (0600, atomic).
expiry selects how the entry records its lifetime -- see :class:TokenExpiryDisposition. There is no default: the caller must decide, so token expiry is never silently fabricated.
#_write_tier
def _write_tier(path: Path, name: str, *, tier: str | None=None, subscription: str | None=None) -> NoneMerge/create a tier metadata entry in the tokens.json at path.
#TokenStoreError
Raised when a tokens.json cannot be read/parsed and resolution cannot proceed.
#TokenStore
Path-injected read/write facade over a single tokens.json file.
All paths are explicit -- TokenStore never reads module path constants. Read APIs (load/token_for/names/expiry_for) raise TokenStoreError on a corrupt or unreadable file. Write APIs (add/set_tier) preserve the historical OSError contract; rename/remove swallow read errors and return False, mirroring the profile_ops helpers they replace.
#load
def load(self) -> dict[str, Any]Parse the tokens.json. Missing -> {}; corrupt/unreadable -> TokenStoreError.
#token_for
def token_for(self, name: str) -> str | NoneReturn the token string for name, or None if absent/tier-only.
#plan_env_for
def plan_env_for(self, name: str) -> dict[str, str]Return name's declared plan tier as Claude Code env vars.
Reads the subscriptionType and rateLimitTier fields of the entry, mapping each present one to its CLAUDE_CODE_* variable. An entry that declares neither yields an empty dict; a legacy bare-string entry likewise yields one.
Declared values are validated against the sets Claude Code actually compares against, because a value it does not recognize behaves exactly like no value at all -- the tier resolves to null and the failure looks identical to not having configured anything. Raises :class:ValueError naming the offending field and the accepted values.
#names
def names(self) -> set[str]Return the set of profile names present in the file.
#expiry_for
def expiry_for(self, name: str) -> TokenExpiry | NoneCompute name's expiry, using the file mtime for legacy entries.
Returns None when the entry is absent. Raises TokenStoreError if the file is corrupt (via load()).
#add
def add(self, name: str, token: str, *, expiry: TokenExpiryDisposition, tier: str | None=None, subscription: str | None=None) -> NoneAdd/update a token entry (0600, atomic). OSError on corrupt file.
expiry is required: the caller must choose how the token's lifetime is recorded (see :class:TokenExpiryDisposition).
#set_tier
def set_tier(self, name: str, *, tier: str | None=None, subscription: str | None=None) -> NoneMerge/create tier metadata (0600, atomic). OSError on corrupt file.
#rename
def rename(self, old: str, new: str) -> boolMove the old key to new. Returns True if the entry existed.
#remove
def remove(self, name: str) -> boolRemove name's entry. Returns True if it existed.