Files
riotpiao.com/lib/auth.ts
T
Story Crater Bot d5f97d2073
Build & Push Portfolio Image / build-push (push) Successful in 3m26s
fix: add error handling and local dev fallback for auth
- Handle getAccessToken failure gracefully
- Support LLM_API_TOKEN env var for local dev (bypasses OAuth)
- Better error messages for missing credentials
2026-09-03 20:38:42 -07:00

77 lines
1.9 KiB
TypeScript

/**
* Authentik OAuth client for service account authentication.
* Uses client_credentials grant with token caching.
*/
interface TokenResponse {
access_token: string
token_type: string
expires_in: number
}
interface CachedToken {
token: string
expiresAt: number
}
let cachedToken: CachedToken | null = null
/**
* Get a valid access token, fetching a new one if expired.
* Tokens are cached with 60s buffer before expiry.
*
* For local dev: set LLM_API_TOKEN env var to skip OAuth.
*/
export async function getAccessToken(): Promise<string> {
// Local dev fallback - use static token if set
const staticToken = process.env.LLM_API_TOKEN
if (staticToken) {
return staticToken
}
const now = Date.now()
// Return cached token if still valid (with 60s buffer)
if (cachedToken && cachedToken.expiresAt > now + 60000) {
return cachedToken.token
}
const clientId = process.env.AUTHENTIK_CLIENT_ID
const clientSecret = process.env.AUTHENTIK_CLIENT_SECRET
const tokenUrl = process.env.AUTHENTIK_TOKEN_URL || 'https://authentik.riotpiao.com/application/o/token/'
if (!clientId || !clientSecret) {
throw new Error(
'Missing OAuth credentials. Set AUTHENTIK_CLIENT_ID + AUTHENTIK_CLIENT_SECRET, ' +
'or LLM_API_TOKEN for local dev.'
)
}
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
scope: 'openid roles',
}),
})
if (!response.ok) {
const error = await response.text()
throw new Error(`Failed to get access token: ${response.status} - ${error}`)
}
const data: TokenResponse = await response.json()
cachedToken = {
token: data.access_token,
expiresAt: now + (data.expires_in * 1000),
}
return cachedToken.token
}