summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-01 20:25:24 -0400
committerGitHub <[email protected]>2026-04-02 00:25:24 +0000
commit00d6841f8474676052553d6278c1ad52b8ecf182 (patch)
treed264db40ed614a16fd06c6e36120e2870af850e5 /packages
parent8a8f7b3e90a247295fa89b66b8a854ce14042b32 (diff)
downloadopencode-00d6841f8474676052553d6278c1ad52b8ecf182.tar.gz
opencode-00d6841f8474676052553d6278c1ad52b8ecf182.zip
fix(account): refresh console tokens before expiry (#20558)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/account/index.ts9
-rw-r--r--packages/opencode/test/account/service.test.ts50
2 files changed, 54 insertions, 5 deletions
diff --git a/packages/opencode/src/account/index.ts b/packages/opencode/src/account/index.ts
index e063eaab5..947fadad0 100644
--- a/packages/opencode/src/account/index.ts
+++ b/packages/opencode/src/account/index.ts
@@ -119,6 +119,7 @@ class TokenRefreshRequest extends Schema.Class<TokenRefreshRequest>("TokenRefres
}) {}
const clientId = "opencode-cli"
+const eagerRefreshThreshold = Duration.minutes(5)
const mapAccountServiceError =
(message = "Account service operation failed") =>
@@ -218,7 +219,9 @@ export namespace Account {
const account = maybeAccount.value
const now = yield* Clock.currentTimeMillis
- if (account.token_expiry && account.token_expiry > now) return account.access_token
+ if (account.token_expiry && account.token_expiry > now + Duration.toMillis(eagerRefreshThreshold)) {
+ return account.access_token
+ }
return yield* refreshToken(account)
}),
@@ -226,7 +229,9 @@ export namespace Account {
const resolveToken = Effect.fnUntraced(function* (row: AccountRow) {
const now = yield* Clock.currentTimeMillis
- if (row.token_expiry && row.token_expiry > now) return row.access_token
+ if (row.token_expiry && row.token_expiry > now + Duration.toMillis(eagerRefreshThreshold)) {
+ return row.access_token
+ }
return yield* Cache.get(refreshTokenCache, row.id)
})
diff --git a/packages/opencode/test/account/service.test.ts b/packages/opencode/test/account/service.test.ts
index a08fce03c..e4c43a1f6 100644
--- a/packages/opencode/test/account/service.test.ts
+++ b/packages/opencode/test/account/service.test.ts
@@ -63,7 +63,7 @@ it.live("orgsByAccount groups orgs per account", () =>
url: "https://one.example.com",
accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"),
- expiry: Date.now() + 60_000,
+ expiry: Date.now() + 10 * 60_000,
orgID: Option.none(),
}),
)
@@ -75,7 +75,7 @@ it.live("orgsByAccount groups orgs per account", () =>
url: "https://two.example.com",
accessToken: AccessToken.make("at_2"),
refreshToken: RefreshToken.make("rt_2"),
- expiry: Date.now() + 60_000,
+ expiry: Date.now() + 10 * 60_000,
orgID: Option.none(),
}),
)
@@ -148,6 +148,50 @@ it.live("token refresh persists the new token", () =>
}),
)
+it.live("token refreshes before expiry when inside the eager refresh window", () =>
+ Effect.gen(function* () {
+ const id = AccountID.make("user-1")
+
+ yield* AccountRepo.use((r) =>
+ r.persistAccount({
+ id,
+ email: "[email protected]",
+ url: "https://one.example.com",
+ accessToken: AccessToken.make("at_old"),
+ refreshToken: RefreshToken.make("rt_old"),
+ expiry: Date.now() + 60_000,
+ orgID: Option.none(),
+ }),
+ )
+
+ let refreshCalls = 0
+ const client = HttpClient.make((req) =>
+ Effect.promise(async () => {
+ if (req.url === "https://one.example.com/auth/device/token") {
+ refreshCalls += 1
+ return json(req, {
+ access_token: "at_new",
+ refresh_token: "rt_new",
+ expires_in: 60,
+ })
+ }
+
+ return json(req, {}, 404)
+ }),
+ )
+
+ const token = yield* Account.Service.use((s) => s.token(id)).pipe(Effect.provide(live(client)))
+
+ expect(String(Option.getOrThrow(token))).toBe("at_new")
+ expect(refreshCalls).toBe(1)
+
+ const row = yield* AccountRepo.use((r) => r.getRow(id))
+ const value = Option.getOrThrow(row)
+ expect(value.access_token).toBe(AccessToken.make("at_new"))
+ expect(value.refresh_token).toBe(RefreshToken.make("rt_new"))
+ }),
+)
+
it.live("concurrent config and token requests coalesce token refresh", () =>
Effect.gen(function* () {
const id = AccountID.make("user-1")
@@ -223,7 +267,7 @@ it.live("config sends the selected org header", () =>
url: "https://one.example.com",
accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"),
- expiry: Date.now() + 60_000,
+ expiry: Date.now() + 10 * 60_000,
orgID: Option.none(),
}),
)