Skip to content

Commit f855b4c

Browse files
kitlangtonbalcsida
authored andcommitted
fix(account): refresh console tokens before expiry (anomalyco#20558)
1 parent 7a7cb53 commit f855b4c

File tree

2 files changed

+54
-5
lines changed

2 files changed

+54
-5
lines changed

packages/opencode/src/account/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class TokenRefreshRequest extends Schema.Class<TokenRefreshRequest>("TokenRefres
119119
}) {}
120120

121121
const clientId = "opencode-cli"
122+
const eagerRefreshThreshold = Duration.minutes(5)
122123

123124
const mapAccountServiceError =
124125
(message = "Account service operation failed") =>
@@ -218,15 +219,19 @@ export namespace Account {
218219

219220
const account = maybeAccount.value
220221
const now = yield* Clock.currentTimeMillis
221-
if (account.token_expiry && account.token_expiry > now) return account.access_token
222+
if (account.token_expiry && account.token_expiry > now + Duration.toMillis(eagerRefreshThreshold)) {
223+
return account.access_token
224+
}
222225

223226
return yield* refreshToken(account)
224227
}),
225228
})
226229

227230
const resolveToken = Effect.fnUntraced(function* (row: AccountRow) {
228231
const now = yield* Clock.currentTimeMillis
229-
if (row.token_expiry && row.token_expiry > now) return row.access_token
232+
if (row.token_expiry && row.token_expiry > now + Duration.toMillis(eagerRefreshThreshold)) {
233+
return row.access_token
234+
}
230235

231236
return yield* Cache.get(refreshTokenCache, row.id)
232237
})

packages/opencode/test/account/service.test.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ it.live("orgsByAccount groups orgs per account", () =>
6363
url: "https://one.example.com",
6464
accessToken: AccessToken.make("at_1"),
6565
refreshToken: RefreshToken.make("rt_1"),
66-
expiry: Date.now() + 60_000,
66+
expiry: Date.now() + 10 * 60_000,
6767
orgID: Option.none(),
6868
}),
6969
)
@@ -75,7 +75,7 @@ it.live("orgsByAccount groups orgs per account", () =>
7575
url: "https://two.example.com",
7676
accessToken: AccessToken.make("at_2"),
7777
refreshToken: RefreshToken.make("rt_2"),
78-
expiry: Date.now() + 60_000,
78+
expiry: Date.now() + 10 * 60_000,
7979
orgID: Option.none(),
8080
}),
8181
)
@@ -148,6 +148,50 @@ it.live("token refresh persists the new token", () =>
148148
}),
149149
)
150150

151+
it.live("token refreshes before expiry when inside the eager refresh window", () =>
152+
Effect.gen(function* () {
153+
const id = AccountID.make("user-1")
154+
155+
yield* AccountRepo.use((r) =>
156+
r.persistAccount({
157+
id,
158+
email: "user@example.com",
159+
url: "https://one.example.com",
160+
accessToken: AccessToken.make("at_old"),
161+
refreshToken: RefreshToken.make("rt_old"),
162+
expiry: Date.now() + 60_000,
163+
orgID: Option.none(),
164+
}),
165+
)
166+
167+
let refreshCalls = 0
168+
const client = HttpClient.make((req) =>
169+
Effect.promise(async () => {
170+
if (req.url === "https://one.example.com/auth/device/token") {
171+
refreshCalls += 1
172+
return json(req, {
173+
access_token: "at_new",
174+
refresh_token: "rt_new",
175+
expires_in: 60,
176+
})
177+
}
178+
179+
return json(req, {}, 404)
180+
}),
181+
)
182+
183+
const token = yield* Account.Service.use((s) => s.token(id)).pipe(Effect.provide(live(client)))
184+
185+
expect(String(Option.getOrThrow(token))).toBe("at_new")
186+
expect(refreshCalls).toBe(1)
187+
188+
const row = yield* AccountRepo.use((r) => r.getRow(id))
189+
const value = Option.getOrThrow(row)
190+
expect(value.access_token).toBe(AccessToken.make("at_new"))
191+
expect(value.refresh_token).toBe(RefreshToken.make("rt_new"))
192+
}),
193+
)
194+
151195
it.live("concurrent config and token requests coalesce token refresh", () =>
152196
Effect.gen(function* () {
153197
const id = AccountID.make("user-1")
@@ -223,7 +267,7 @@ it.live("config sends the selected org header", () =>
223267
url: "https://one.example.com",
224268
accessToken: AccessToken.make("at_1"),
225269
refreshToken: RefreshToken.make("rt_1"),
226-
expiry: Date.now() + 60_000,
270+
expiry: Date.now() + 10 * 60_000,
227271
orgID: Option.none(),
228272
}),
229273
)

0 commit comments

Comments
 (0)