Build & Push Portfolio Image / build-push (push) Successful in 3m26s
- Handle getAccessToken failure gracefully - Support LLM_API_TOKEN env var for local dev (bypasses OAuth) - Better error messages for missing credentials
77 lines
1.9 KiB
TypeScript
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
|
|
}
|