import { trace, type Tracer } from "@opentelemetry/api" import { getCacheClient } from "../dataCache" import { env } from "../env/server" import { createCounter } from "../telemetry" interface ServiceTokenResponse { access_token: string scope?: string token_type: string expires_in: number } export async function getServiceToken() { const tracer = trace.getTracer("getServiceToken") return await tracer.startActiveSpan("getServiceToken", async () => { const scopes = env.CURITY_CLIENT_SERVICE_SCOPES const cacheKey = getServiceTokenCacheKey(scopes) const cacheClient = await getCacheClient() const token = await getOrSetServiceTokenFromCache(cacheKey, scopes, tracer) if (token.expiresAt < Date.now()) { await cacheClient.deleteKey(cacheKey) const newToken = await getOrSetServiceTokenFromCache( cacheKey, scopes, tracer ) return newToken.jwt } return token.jwt }) } async function getOrSetServiceTokenFromCache( cacheKey: string, scopes: string[], tracer: Tracer ) { const cacheClient = await getCacheClient() const token = await cacheClient.cacheOrGet( cacheKey, async () => { return await tracer.startActiveSpan("fetch new token", async () => { const newToken = await getJwt(scopes) return newToken }) }, "1h" ) return token } async function getJwt(scopes: string[]) { const getJwtCounter = createCounter("tokenManager", "getJwt") const metricsGetJwt = getJwtCounter.init({ scopes, }) metricsGetJwt.start() const jwt = await fetchServiceToken(scopes) const expiresAt = Date.now() + jwt.expires_in * 1000 metricsGetJwt.success() return { expiresAt, jwt } } async function fetchServiceToken(scopes: string[]) { const fetchServiceTokenCounter = createCounter( "tokenManager", "fetchServiceToken" ) const metricsFetchServiceToken = fetchServiceTokenCounter.init({ scopes, }) metricsFetchServiceToken.start() const response = await fetch(`${env.CURITY_ISSUER_USER}/oauth/v2/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json", }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: env.CURITY_CLIENT_ID_SERVICE, client_secret: env.CURITY_CLIENT_SECRET_SERVICE, scope: scopes.join(" "), }), signal: AbortSignal.timeout(15_000), }) if (!response.ok) { await metricsFetchServiceToken.httpError(response) const text = await response.text() throw new Error( `[fetchServiceToken] Failed to obtain service token: ${JSON.stringify({ status: response.status, statusText: response.statusText, text, })}` ) } const result = response.json() as Promise metricsFetchServiceToken.success() return result } function getServiceTokenCacheKey(scopes: string[]): string { return `serviceToken:${scopes.join(",")}` }