Skip to content

Commit f4c00bc

Browse files
committed
fix(credentials): time a reconnected token's expiry caption from now and reject out-of-range GitHub offsets
1 parent 900fdcb commit f4c00bc

4 files changed

Lines changed: 38 additions & 6 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/integrations/hooks/use-saved-token-expiry-label.test.tsx‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,28 @@ describe('useSavedTokenExpiryLabel', () => {
5555
expect(label).toBe('expires in 1 day')
5656
})
5757

58+
it('times a reconnected token from the moment it arrives, not from when the page opened', () => {
59+
mount(new Date(NOW + 3 * DAY_MS).toISOString())
60+
act(() => {
61+
vi.advanceTimersByTime(6 * 60 * 60 * 1000)
62+
})
63+
mount(new Date(Date.now() + 60 * 60 * 1000).toISOString())
64+
expect(label).toBe('expires in 1 day')
65+
act(() => {
66+
vi.advanceTimersByTime(60 * 60 * 1000)
67+
})
68+
expect(label).toBe('expired')
69+
})
70+
71+
it('counts a token that gains an expiry after the page has been open for days from now', () => {
72+
mount(undefined)
73+
act(() => {
74+
vi.advanceTimersByTime(3 * DAY_MS)
75+
})
76+
mount(new Date(Date.now() + 60 * 60 * 1000).toISOString())
77+
expect(label).toBe('expires in 1 day')
78+
})
79+
5880
it('has no caption for a token that states no expiry', () => {
5981
mount(undefined)
6082
expect(label).toBeUndefined()

‎apps/sim/app/workspace/[workspaceId]/integrations/hooks/use-saved-token-expiry-label.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@ import {
66

77
/**
88
* The caption for a saved token's expiry, kept current while the page stays open: it re-reads the
9-
* clock when the day count drops and when the token expires. `undefined` when the token states no
10-
* readable expiry.
9+
* clock when the expiry changes (a reconnect), when the day count drops, and when the token
10+
* expires. `undefined` when the token states no readable expiry.
1111
*/
1212
export function useSavedTokenExpiryLabel(expiresAt: string | undefined): string | undefined {
13-
const [now, setNow] = useState(() => Date.now())
1413
const expiresAtMs = expiresAt ? new Date(expiresAt).getTime() : Number.NaN
14+
const [clock, setClock] = useState(() => ({ expiresAtMs, now: Date.now() }))
15+
if (!Object.is(clock.expiresAtMs, expiresAtMs)) {
16+
setClock({ expiresAtMs, now: Date.now() })
17+
}
18+
const { now } = clock
1519

1620
useEffect(() => {
1721
const untilChange = msUntilSavedTokenExpiryLabelChanges(new Date(expiresAtMs), now)
1822
if (untilChange === null) return
19-
const timer = setTimeout(() => setNow(Date.now()), untilChange)
23+
const timer = setTimeout(
24+
() => setClock((current) => ({ ...current, now: Date.now() })),
25+
untilChange
26+
)
2027
return () => clearTimeout(timer)
2128
}, [expiresAtMs, now])
2229

‎apps/sim/lib/credentials/token-service-accounts/validators/github.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ describe('GitHub saved personal access token verification', () => {
109109
['a zone name other than UTC', '2026-12-01 10:30:00 PST'],
110110
['a lowercase utc marker', '2026-12-01 10:30:00 utc'],
111111
['an impossible date', '2026-13-45 10:00:00 +0000'],
112+
['an offset hour past 23', '2026-12-01 10:30:00 +2400'],
113+
['an offset minute past 59', '2026-12-01 10:30:00 -0060'],
112114
['an expiry in the past, contradicting the accepted request', '2026-11-10 12:00:00 +0000'],
113115
])('records no expiry for %s and still accepts the token', async (_, header) => {
114116
fetch.mockResolvedValue(userReply(header))

‎apps/sim/lib/credentials/vendor-token-expiry.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ function calendarMomentUtc(
3030
* `2026-12-01 12:30:00 +0200` or `2026-12-01 10:30:00 UTC`. GitHub documents the header but not
3131
* its value format, so this is parsed strictly by design: the two forms accepted are the ones in
3232
* the API responses GitHub's own client library records (octokit/octokit.rb spec cassettes), and
33-
* any other form is left unread.
33+
* any other form (including an offset past 23 hours or 59 minutes) is left unread.
3434
*/
35-
const GITHUB_TOKEN_EXPIRATION = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) ([+-]\d{4}|UTC)$/
35+
const GITHUB_TOKEN_EXPIRATION =
36+
/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}) ([+-](?:[01]\d|2[0-3])[0-5]\d|UTC)$/
3637

3738
/**
3839
* Reads GitHub's `GitHub-Authentication-Token-Expiration` response header, which GitHub sends on

0 commit comments

Comments
 (0)