lobehub/apps/cli/src/auth/refresh.ts
Rylan Cai 4dd271c968
feat(cli): support api key auth in cli (#13190)
*  support cli api key auth

* 🔒 reject invalid x-api-key without fallback auth

* ♻️ clean up cli api key auth diff

* ♻️ clean up cli auth command diff

* ♻️ clean up remaining cli auth diff

* ♻️ split stored auth token fields

* ♻️ trim connect auth surface

* ♻️ drop redundant jwt user id carry-over

* ♻️ trim auth test wording diff

* 🐛 fix api key model imports

* 🐛 fix api key util subpath import

* 🔐 chore(cli): use env-only api key auth

* ♻️ refactor(cli): simplify auth credential flow

*  feat: simplify cli api key login flow

* 🐛 fix(cli): prefer jwt for webapi auth

* ♻️ refactor(cli): trim auth http diff

* 🐛 fix(cli): skip api key auth expiry handling

* 🐛 fix(cli): restore non-jwt expiry handling

* ♻️ refactor(cli): trim connect auth expired diff

* ♻️ refactor(cli): trim login comment diff

* ♻️ refactor(cli): trim resolve token comment diff

* ♻️ refactor(cli): restore connect expiry flow

* ♻️ refactor(cli): trim login api key message

* 🐛 fix(cli): support api key gateway auth

* ♻️ refactor(cli): restore resolve token comment

* ♻️ refactor(cli): trim test-only auth diffs

* ♻️ refactor(cli): restore resolve token comments

*  test(cli): add api key expiry coverage

* 🐛 fix cli auth server resolution and gateway auth

* ♻️ prune auth fix diff noise

* ♻️ unify cli server url precedence

* ♻️ simplify device gateway auth tests

*  add gateway auth edge case coverage

*  remove low-value gateway auth test

* 🐛 fix api key context test mock typing
2026-03-26 10:11:38 +08:00

68 lines
1.9 KiB
TypeScript

import { resolveServerUrl } from '../settings';
import { loadCredentials, saveCredentials, type StoredCredentials } from './credentials';
const CLIENT_ID = 'lobehub-cli';
/**
* Get a valid access token, refreshing if expired.
* Returns null if no credentials or refresh fails.
*/
export async function getValidToken(): Promise<{ credentials: StoredCredentials } | null> {
const credentials = loadCredentials();
if (!credentials) return null;
// Check if token is still valid (with 60s buffer)
if (credentials.expiresAt && Date.now() / 1000 < credentials.expiresAt - 60) {
return { credentials };
}
// Token expired — try refresh
if (!credentials.refreshToken) return null;
const serverUrl = resolveServerUrl();
const refreshed = await refreshAccessToken(serverUrl, credentials.refreshToken);
if (!refreshed) return null;
const updated: StoredCredentials = {
accessToken: refreshed.access_token,
expiresAt: refreshed.expires_in
? Math.floor(Date.now() / 1000) + refreshed.expires_in
: undefined,
refreshToken: refreshed.refresh_token || credentials.refreshToken,
};
saveCredentials(updated);
return { credentials: updated };
}
interface TokenResponse {
access_token: string;
expires_in?: number;
refresh_token?: string;
token_type: string;
}
async function refreshAccessToken(
serverUrl: string,
refreshToken: string,
): Promise<TokenResponse | null> {
try {
const res = await fetch(`${serverUrl}/oidc/token`, {
body: new URLSearchParams({
client_id: CLIENT_ID,
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
method: 'POST',
});
const body = (await res.json()) as TokenResponse & { error?: string };
if (!res.ok || body.error || !body.access_token) return null;
return body;
} catch {
return null;
}
}