summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
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(),
}),
)