Repository navigation
feat(credentials): record, show, and explain saved token expiry #9930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
waleedlatif1
merged 13 commits into
staging
from
feat/credential-expiry-and-slack-rotation
Nov 12, 2026
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
3572f3a
feat(credentials): record when a saved GitHub or GitLab token expires
waleedlatif1 36cd897
feat(credentials): say a saved token expired when its provider refuse…
waleedlatif1 2442d2e
feat(integrations): show when a saved token expires
waleedlatif1 f0df3e5
refactor(credentials): keep the expired saved-token error private to …
waleedlatif1 271d348
refactor(credentials): share the vendor token expiry readers with the…
waleedlatif1 123daee
feat(credentials): report a saved token's expiry on organization cred…
waleedlatif1 b95575e
fix(credentials): log an expired saved token's refusal as the provide…
waleedlatif1 96ef87c
fix(integrations): show a saved token's expiry time in UTC
waleedlatif1 f670711
fix(credentials): keep an imminent stated token expiry and read only …
waleedlatif1 c901a86
fix(credentials): say an expired saved token's date is UTC and keep i…
waleedlatif1 6fafaf9
fix(credentials): read GitHub's UTC-marked token expiration header form
waleedlatif1 900fdcb
fix(credentials): keep saved-token expiry captions current and drop a…
waleedlatif1 f4c00bc
fix(credentials): time a reconnected token's expiry caption from now …
waleedlatif1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
85 changes: 85 additions & 0 deletions
85
.../sim/app/workspace/[workspaceId]/integrations/hooks/use-saved-token-expiry-label.test.tsx
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| /** | ||
| * @vitest-environment jsdom | ||
| */ | ||
|
|
||
| import { act } from 'react' | ||
| import { createRoot, type Root } from 'react-dom/client' | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' | ||
| import { useSavedTokenExpiryLabel } from '@/app/workspace/[workspaceId]/integrations/hooks/use-saved-token-expiry-label' | ||
|
|
||
| const NOW = Date.parse('2026-11-11T12:00:00.000Z') | ||
| const DAY_MS = 24 * 60 * 60 * 1000 | ||
|
|
||
| let container: HTMLDivElement | ||
| let root: Root | ||
| let label: string | undefined | ||
|
|
||
| function Harness({ expiresAt }: { expiresAt?: string }) { | ||
| label = useSavedTokenExpiryLabel(expiresAt) | ||
| return null | ||
| } | ||
|
|
||
| function mount(expiresAt?: string) { | ||
| act(() => { | ||
| root.render(<Harness expiresAt={expiresAt} />) | ||
| }) | ||
| } | ||
|
|
||
| describe('useSavedTokenExpiryLabel', () => { | ||
| beforeEach(() => { | ||
| vi.useFakeTimers({ now: NOW }) | ||
| container = document.createElement('div') | ||
| root = createRoot(container) | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| act(() => root.unmount()) | ||
| vi.useRealTimers() | ||
| }) | ||
|
|
||
| it('reads expired once the token expires while the page stays open', () => { | ||
| mount(new Date(NOW + 60_000).toISOString()) | ||
| expect(label).toBe('expires in 1 day') | ||
| act(() => { | ||
| vi.advanceTimersByTime(60_000) | ||
| }) | ||
| expect(label).toBe('expired') | ||
| }) | ||
|
|
||
| it('counts the days down while the page stays open', () => { | ||
| mount(new Date(NOW + 2 * DAY_MS).toISOString()) | ||
| expect(label).toBe('expires in 2 days') | ||
| act(() => { | ||
| vi.advanceTimersByTime(DAY_MS + 1) | ||
| }) | ||
| expect(label).toBe('expires in 1 day') | ||
| }) | ||
|
|
||
| it('times a reconnected token from the moment it arrives, not from when the page opened', () => { | ||
| mount(new Date(NOW + 3 * DAY_MS).toISOString()) | ||
| act(() => { | ||
| vi.advanceTimersByTime(6 * 60 * 60 * 1000) | ||
| }) | ||
| mount(new Date(Date.now() + 60 * 60 * 1000).toISOString()) | ||
| expect(label).toBe('expires in 1 day') | ||
| act(() => { | ||
| vi.advanceTimersByTime(60 * 60 * 1000) | ||
| }) | ||
| expect(label).toBe('expired') | ||
| }) | ||
|
|
||
| it('counts a token that gains an expiry after the page has been open for days from now', () => { | ||
| mount(undefined) | ||
| act(() => { | ||
| vi.advanceTimersByTime(3 * DAY_MS) | ||
| }) | ||
| mount(new Date(Date.now() + 60 * 60 * 1000).toISOString()) | ||
| expect(label).toBe('expires in 1 day') | ||
| }) | ||
|
|
||
| it('has no caption for a token that states no expiry', () => { | ||
| mount(undefined) | ||
| expect(label).toBeUndefined() | ||
| expect(vi.getTimerCount()).toBe(0) | ||
| }) | ||
| }) |
33 changes: 33 additions & 0 deletions
33
apps/sim/app/workspace/[workspaceId]/integrations/hooks/use-saved-token-expiry-label.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { useEffect, useState } from 'react' | ||
| import { | ||
| msUntilSavedTokenExpiryLabelChanges, | ||
| savedTokenExpiryLabel, | ||
| } from '@/lib/credentials/expiry-label' | ||
|
|
||
| /** | ||
| * The caption for a saved token's expiry, kept current while the page stays open: it re-reads the | ||
| * clock when the expiry changes (a reconnect), when the day count drops, and when the token | ||
| * expires. `undefined` when the token states no readable expiry. | ||
| */ | ||
| export function useSavedTokenExpiryLabel(expiresAt: string | undefined): string | undefined { | ||
| const expiresAtMs = expiresAt ? new Date(expiresAt).getTime() : Number.NaN | ||
| const [clock, setClock] = useState(() => ({ expiresAtMs, now: Date.now() })) | ||
| if (!Object.is(clock.expiresAtMs, expiresAtMs)) { | ||
| setClock({ expiresAtMs, now: Date.now() }) | ||
| } | ||
| const { now } = clock | ||
|
|
||
| useEffect(() => { | ||
| const untilChange = msUntilSavedTokenExpiryLabelChanges(new Date(expiresAtMs), now) | ||
| if (untilChange === null) return | ||
| const timer = setTimeout( | ||
| () => setClock((current) => ({ ...current, now: Date.now() })), | ||
| untilChange | ||
| ) | ||
| return () => clearTimeout(timer) | ||
| }, [expiresAtMs, now]) | ||
|
|
||
| return Number.isFinite(expiresAtMs) | ||
| ? savedTokenExpiryLabel(new Date(expiresAtMs), now) | ||
| : undefined | ||
| } | ||
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.