fix(account): refresh console tokens before expiry (#20558)
parent
8a8f7b3e90
commit
00d6841f84
|
|
@ -119,6 +119,7 @@ class TokenRefreshRequest extends Schema.Class<TokenRefreshRequest>("TokenRefres
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
const clientId = "opencode-cli"
|
const clientId = "opencode-cli"
|
||||||
|
const eagerRefreshThreshold = Duration.minutes(5)
|
||||||
|
|
||||||
const mapAccountServiceError =
|
const mapAccountServiceError =
|
||||||
(message = "Account service operation failed") =>
|
(message = "Account service operation failed") =>
|
||||||
|
|
@ -218,7 +219,9 @@ export namespace Account {
|
||||||
|
|
||||||
const account = maybeAccount.value
|
const account = maybeAccount.value
|
||||||
const now = yield* Clock.currentTimeMillis
|
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)
|
return yield* refreshToken(account)
|
||||||
}),
|
}),
|
||||||
|
|
@ -226,7 +229,9 @@ export namespace Account {
|
||||||
|
|
||||||
const resolveToken = Effect.fnUntraced(function* (row: AccountRow) {
|
const resolveToken = Effect.fnUntraced(function* (row: AccountRow) {
|
||||||
const now = yield* Clock.currentTimeMillis
|
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)
|
return yield* Cache.get(refreshTokenCache, row.id)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ it.live("orgsByAccount groups orgs per account", () =>
|
||||||
url: "https://one.example.com",
|
url: "https://one.example.com",
|
||||||
accessToken: AccessToken.make("at_1"),
|
accessToken: AccessToken.make("at_1"),
|
||||||
refreshToken: RefreshToken.make("rt_1"),
|
refreshToken: RefreshToken.make("rt_1"),
|
||||||
expiry: Date.now() + 60_000,
|
expiry: Date.now() + 10 * 60_000,
|
||||||
orgID: Option.none(),
|
orgID: Option.none(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -75,7 +75,7 @@ it.live("orgsByAccount groups orgs per account", () =>
|
||||||
url: "https://two.example.com",
|
url: "https://two.example.com",
|
||||||
accessToken: AccessToken.make("at_2"),
|
accessToken: AccessToken.make("at_2"),
|
||||||
refreshToken: RefreshToken.make("rt_2"),
|
refreshToken: RefreshToken.make("rt_2"),
|
||||||
expiry: Date.now() + 60_000,
|
expiry: Date.now() + 10 * 60_000,
|
||||||
orgID: Option.none(),
|
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: "user@example.com",
|
||||||
|
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", () =>
|
it.live("concurrent config and token requests coalesce token refresh", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const id = AccountID.make("user-1")
|
const id = AccountID.make("user-1")
|
||||||
|
|
@ -223,7 +267,7 @@ it.live("config sends the selected org header", () =>
|
||||||
url: "https://one.example.com",
|
url: "https://one.example.com",
|
||||||
accessToken: AccessToken.make("at_1"),
|
accessToken: AccessToken.make("at_1"),
|
||||||
refreshToken: RefreshToken.make("rt_1"),
|
refreshToken: RefreshToken.make("rt_1"),
|
||||||
expiry: Date.now() + 60_000,
|
expiry: Date.now() + 10 * 60_000,
|
||||||
orgID: Option.none(),
|
orgID: Option.none(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue