Repository navigation
fix(oauth): parse numeric epoch expiresAt so imported connections still refresh - #3997
Open
elielsousa-pathbit wants to merge 1 commit into
Open
elielsousa-pathbit wants to merge 1 commit into
elielsousa-pathbit wants to merge 1 commit into
Conversation
…ll refresh
parseTimeMs() accepted numbers and date-like strings, but a numeric epoch in
string form ("1789012345678") went down the Date branch, where it is an
Invalid Date, and returned null.
A null expiry silently disables both proactive refresh paths:
- shouldRefreshCredentials() returns false, so the on-request refresh in
checkAndRefreshToken() never fires;
- selectConnectionsNeedingRefresh() skips the connection, so the background
sweep never fires either.
The connection then keeps an expired access token and 401s until the user
re-authenticates by hand - the same symptom as decolua#2546, reached through a
different input shape.
Numeric epochs reach the DB unnormalized through the bulk-import routes, which
persist the user-supplied value verbatim:
src/app/api/oauth/grok-cli/bulk-import/route.js:77 (raw.expires_at)
src/app/api/oauth/codex/bulk-import/route.js:97 (item.expiresAt)
compare src/lib/oauth/kiroExternalIdp.js:57, which already normalizes to ISO.
parseTimeMs() now converts numeric strings with the same seconds/ms heuristic
it already applies to numbers, and is exported so normalizeExpiresAt() in
src/sse/services/tokenRefresh.js reuses it - a numeric epoch that is already
stored now self-heals to ISO on the next refresh.
7 tasks done
bloodf
pushed a commit
to bloodf/durindoor
that referenced
this pull request
Sep 17, 2026
…ric epoch string
`parseTimeMs` handled numbers and date-like strings, but `new Date("1789012345678")`
is an Invalid Date, so a numeric epoch stored as text resolved to no expiry at
all. With a null expiry `shouldRefreshCredentials` returns false, the proactive
refresh never fires, and the connection keeps serving an expired access token
until someone re-authorizes it by hand.
The shape is reachable: both OAuth bulk-import routes persist the user-supplied
value verbatim and only synthesize an ISO string when the field is absent —
src/app/api/oauth/grok-cli/bulk-import/route.js:79 and
src/app/api/oauth/codex/bulk-import/route.js:110.
Numeric text is now converted numerically before the Date fallback, and
normalizeExpiresAt routes through the same parser so an affected connection
self-heals to ISO form on its next refresh.
Upstream: decolua/9router#3997
bloodf
pushed a commit
to bloodf/durindoor
that referenced
this pull request
Sep 17, 2026
…ric epoch string
`parseTimeMs` handled numbers and date-like strings, but `new Date("1789012345678")`
is an Invalid Date, so a numeric epoch stored as text resolved to no expiry at
all. With a null expiry `shouldRefreshCredentials` returns false, the proactive
refresh never fires, and the connection keeps serving an expired access token
until someone re-authorizes it by hand.
The shape is reachable: both OAuth bulk-import routes persist the user-supplied
value verbatim and only synthesize an ISO string when the field is absent —
src/app/api/oauth/grok-cli/bulk-import/route.js:79 and
src/app/api/oauth/codex/bulk-import/route.js:110.
Numeric text is now converted numerically before the Date fallback, and
normalizeExpiresAt routes through the same parser so an affected connection
self-heals to ISO form on its next refresh.
Upstream: decolua/9router#3997
bloodf
added a commit
to bloodf/durindoor
that referenced
this pull request
Sep 17, 2026
…ric epoch string (#882) `parseTimeMs` handled numbers and date-like strings, but `new Date("1789012345678")` is an Invalid Date, so a numeric epoch stored as text resolved to no expiry at all. With a null expiry `shouldRefreshCredentials` returns false, the proactive refresh never fires, and the connection keeps serving an expired access token until someone re-authorizes it by hand. The shape is reachable: both OAuth bulk-import routes persist the user-supplied value verbatim and only synthesize an ISO string when the field is absent — src/app/api/oauth/grok-cli/bulk-import/route.js:79 and src/app/api/oauth/codex/bulk-import/route.js:110. Numeric text is now converted numerically before the Date fallback, and normalizeExpiresAt routes through the same parser so an affected connection self-heals to ISO form on its next refresh. Upstream: decolua/9router#3997 Co-authored-by: durindoor-bot <bot@durindoor.local>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
parseTimeMs()inopen-sse/services/oauthCredentialManager.jsaccepts anumberand anythingDatecan parse, but a numeric epoch in string form falls through to theDatebranch, where it is an Invalid Date:A
nullexpiry silently disables both proactive refresh paths:nullshouldRefreshCredentials()(oauthCredentialManager.js:48-51)expiresAtMs !== nullis false → on-request refresh incheckAndRefreshToken()never firesselectConnectionsNeedingRefresh()(src/sse/services/backgroundTokenRefresh.js:59-60)if (expiresAtMs === null) continue;→ the background sweep skips the connection entirelyThe connection then keeps an expired access token and 401s until the user re-authenticates by hand — the same user-visible symptom as #2546 ("session dies 40-45 min after login"), reached through a different input shape.
Where a numeric epoch comes from
The bulk-import routes persist the user-supplied value verbatim, with no normalization:
src/app/api/oauth/grok-cli/bulk-import/route.js:77—let expiresAt = raw.expires_at || raw.expiresAt || null;src/app/api/oauth/codex/bulk-import/route.js:97— only fillsexpiresAtwhen it is absent; an existingitem.expiresAtpasses through raw.src/app/api/oauth/kiro/import-cli-proxy/route.js:20—expiresAt: tokenData.expiresAtCompare
src/lib/oauth/kiroExternalIdp.js:56-62(resolveExpiresAt), which already normalizes any parseableexpires_atto ISO. The repo has the right convention; the parser just doesn't accept the shape those routes can store.The same applies to any external tool that syncs credentials into
providerConnections.dataand writes the epoch as text.Fix
Minimal and contained to the parser plus one caller:
parseTimeMs()converts numeric strings with the same seconds/ms heuristic it already applies to numbers (< 1e12→ seconds), then falls back toDatefor ISO and other date strings. Non-numeric garbage and empty values still returnnull.parseTimeMs()is exported, andnormalizeExpiresAt()insrc/sse/services/tokenRefresh.jsnow reuses it — so an epoch that is already stored self-heals to ISO on the next refresh instead of being dropped.No behavior change for values that already worked (ISO strings, numbers).
Validation
Failing-then-passing test, plus a full-suite regression diff against
masterin the same checkout.Full
tests/suite, run onmasterand on this branch in the same checkout:master0 regressions — no test that passes on
masterfails here.+6is exactly the new file. The 114 failures are identical on both sides and pre-existing (CLAUDE.mddocuments the suite as not all-green on a plain checkout).Files changed
open-sse/services/oauthCredentialManager.js—parseTimeMs()accepts numeric strings; exported.src/sse/services/tokenRefresh.js—normalizeExpiresAt()routes throughparseTimeMs().tests/unit/credential-expiry-numeric-epoch.test.js— new regression test (6 cases).