fix(core): stop an unreadable token expiry meaning "never expires" - #72
Conversation
tokenIsUsable treated any expires_at it could not parse as no expiry at all:
const expiresAt = parseIsoDate(cacheDocument.expires_at);
if (!expiresAt) {
return true;
}
parseIsoDate returns null for anything it cannot read, so six different shapes
all resolved to "usable forever": absent, null, "", "not-a-date",
1700000000000, and {}.
The numeric case is the one that makes this more than theoretical. Epoch
milliseconds is a standard way to send an expiry, and broker-client stringifies
whatever the broker sends -- so 1700000000000 arrives here as "1700000000000",
which new Date() reads as Invalid Date. If the broker ever emits a numeric
expiry, the CLI caches that token and never refreshes it again. The failure
surfaces much later as auth errors with no re-login, because the client is
certain the token is fine.
Two changes:
- parseIsoDate accepts epoch seconds and milliseconds as well as ISO strings.
It also matches the sign, so a negative value is rejected rather than handed
to new Date(), which does not fail on it -- V8 reads "-1" as a date and
returns 2001-01-01, a nonsense expiry rather than an error.
- tokenIsUsable distinguishes absent from unreadable. Absent still keeps the
token: the broker never committed to an expiry, and that is the existing
behaviour. Present-but-unreadable now forces a refresh, because one extra
login costs far less than a client that is certain about a token it cannot
reason about.
Worth noting the neighbouring pendingIsExpired assumes the opposite for the same
unparseable input, via Boolean(expiresAt && ...). The two now agree that an
unreadable date is not a reason for confidence.
Tests: 8 cases covering both epoch forms, past and future, unreadable values,
absent values, the minTtlSeconds window, and malformed tokens. core 20/20; type
check clean. The codex-plugin failures in the full suite are pre-existing on
main (they are the CRLF issue in CALLE-AI#70) and appear with and without this change.
Ray-56
left a comment
There was a problem hiding this comment.
The expiry parsing and fail-closed behavior look sound in this review, and the Core check, test, typecheck, and pack dry-run all pass locally. One repository release requirement still blocks merge:
[P2] Add a patch changeset for @call-e/core. This changes published token-cache behavior, and CONTRIBUTING.md requires a changeset for published package behavior. Please run pnpm changeset, describe the epoch-expiry parsing and unreadable-expiry refresh behavior, then verify with pnpm run check:versions and pnpm --filter @call-e/core pack:dry-run.
I did not find another security or correctness blocker in the current diff.
Addresses the review on CALLE-AI#72. Describes the epoch-expiry parsing and the unreadable-expiry refresh behaviour, as asked. Verified: pnpm run check:versions is in sync, pnpm --filter @call-e/core pack:dry-run builds the tarball, and the core suite is 20/20.
|
Changeset added for
|
Ray-56
left a comment
There was a problem hiding this comment.
The required @call-e/core patch changeset is now present and accurately describes the token-expiry fix. The implementation still looks sound against the updated main branch, GitHub CI is green, and the local Core check, test, typecheck, version validation, and package dry-run all pass. The previous release blocker is resolved.
tokenIsUsabletreats anyexpires_atit cannot parse as no expiry at all, so the token is cached and never refreshed again.Measured
Six different shapes all resolve to "usable forever":
The numeric case is not contrived. Epoch milliseconds is a standard way to send an expiry, and
broker-client.jsstringifies whatever the broker returns:So
1700000000000arrives at the cache as the string"1700000000000", whichnew Date()reads asInvalid Date. If the broker ever emits a numeric expiry, the CLI caches that token and stops refreshing. It surfaces much later as auth failures with no re-login, because the client is certain the token is still good.Fix
parseIsoDateunderstands epoch values. Seconds and milliseconds, as a number or as the stringbroker-clientproduces. It also matches the sign, so a negative value is rejected in the numeric branch rather than handed tonew Date()— which does not fail on it.new Date("-1")returns2001-01-01, a nonsense expiry rather than an error, and that is pre-existing behaviour this closes on the way past.tokenIsUsabledistinguishes absent from unreadable.Worth flagging that
pendingIsExpired, a few lines below, assumes the opposite for the same unreadable input viaBoolean(expiresAt && ...). The two now agree that an unreadable date is not a reason for confidence.Tests
Eight cases in
packages/core/test/token-expiry.test.js: both epoch forms, past and future, unreadable values, absent values, theminTtlSecondswindow, and malformed tokens.@call-e/core: 20 passing, 0 failingpnpm --filter @call-e/core check: syntax andtsc --strictboth cleanThe
codex-pluginfailures in the full suite are pre-existing onmain— they are the CRLF issue from #70, and they appear identically with and without this change (verified by stashing).Third of three from the same audit, and the only one that is not Windows-specific: #70 (CRLF and path separators), #71 (
auth loginmangled by cmd.exe), this one.Disclosure: written with AI assistance; every measurement above was run on my own machine and I take responsibility for the change.