Skip to content

fix(core): stop an unreadable token expiry meaning "never expires" - #72

Merged
Ray-56 merged 2 commits into
CALLE-AI:mainfrom
EazyHood:fix/token-expiry-fails-open
Aug 4, 2026
Merged

fix(core): stop an unreadable token expiry meaning "never expires"#72
Ray-56 merged 2 commits into
CALLE-AI:mainfrom
EazyHood:fix/token-expiry-fails-open

Conversation

@EazyHood

@EazyHood EazyHood commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

tokenIsUsable treats any expires_at it cannot parse as no expiry at all, so the token is cached and never refreshed again.

const expiresAt = parseIsoDate(cacheDocument.expires_at);
if (!expiresAt) {
  return true;
}

Measured

Six different shapes all resolve to "usable forever":

absent          -> usable
null            -> usable
""              -> usable
"not-a-date"    -> usable
1700000000000   -> usable      <- the realistic one
{}              -> usable

The numeric case is not contrived. Epoch milliseconds is a standard way to send an expiry, and broker-client.js stringifies whatever the broker returns:

expires_at: sessionPayload.expires_at ? String(sessionPayload.expires_at) : null,

So 1700000000000 arrives at the cache as the string "1700000000000", which new Date() reads as Invalid 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

parseIsoDate understands epoch values. Seconds and milliseconds, as a number or as the string broker-client produces. It also matches the sign, so a negative value is rejected in the numeric branch rather than handed to new Date() — which does not fail on it. new Date("-1") returns 2001-01-01, a nonsense expiry rather than an error, and that is pre-existing behaviour this closes on the way past.

tokenIsUsable distinguishes absent from unreadable.

  • Absent still keeps the token. The broker never committed to an expiry, so there is nothing to distrust — unchanged behaviour.
  • Present but unreadable now forces a refresh. One extra login costs far less than a client that is confident about a token it cannot reason about.

Worth flagging that pendingIsExpired, a few lines below, assumes the opposite for the same unreadable input via Boolean(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, the minTtlSeconds window, and malformed tokens.

  • @call-e/core: 20 passing, 0 failing
  • pnpm --filter @call-e/core check: syntax and tsc --strict both clean

The codex-plugin failures in the full suite are pre-existing on main — 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 login mangled 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.

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 Ray-56 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@EazyHood

EazyHood commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Changeset added for @call-e/core, describing the epoch-expiry parsing and the unreadable-expiry refresh.

pnpm run check:versions is in sync and pnpm --filter @call-e/core pack:dry-run builds the tarball. Core suite is 20/20.

@Ray-56 Ray-56 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Ray-56
Ray-56 merged commit 566008b into CALLE-AI:main Aug 4, 2026
1 check passed
@github-actions github-actions Bot mentioned this pull request Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants