UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

189 lines (164 loc) 4.7 kB
import { $context, AlephaError } from "alepha"; import { DateTimeProvider } from "alepha/datetime"; import type { UserAccount } from "../schemas/userAccountInfoSchema.ts"; import type { AccessTokenResponse, IssuerPrimitive } from "./$issuer.ts"; /** * Allow to get an access token for a service account. * * You have some options to configure the service account: * - a OAUTH2 URL using client credentials grant type * - a JWT secret shared between the services * * @example * ```ts * import { $serviceAccount } from "alepha/security"; * * class MyService { * serviceAccount = $serviceAccount({ * oauth2: { * url: "https://example.com/oauth2/token", * clientId: "your-client-id", * clientSecret: "your-client-secret", * } * }); * * async fetchData() { * const token = await this.serviceAccount.token(); * // or * const response = await this.serviceAccount.fetch("https://api.example.com/data"); * } * } * ``` */ export const $serviceAccount = ( options: ServiceAccountPrimitiveOptions, ): ServiceAccountPrimitive => { const { alepha } = $context(); const store: { cache?: AccessTokenResponse; } = {}; const dateTimeProvider = alepha.inject(DateTimeProvider); const gracePeriod = options.gracePeriod ?? 30; const cacheToken = (response: Omit<AccessTokenResponse, "at">) => { store.cache = { ...response, issued_at: dateTimeProvider.now().unix(), }; }; const getTokenFromCache = () => { if (store.cache) { const { access_token, expires_in, issued_at } = store.cache; if (!expires_in) { return access_token; } const now = dateTimeProvider.now().unix(); const expires = issued_at + expires_in; if (expires - gracePeriod > now) { return access_token; } } }; if ("oauth2" in options) { const { url, clientId, clientSecret } = options.oauth2; const token = async () => { const tokenFromCache = getTokenFromCache(); if (tokenFromCache) { return tokenFromCache; } let response: Response; try { response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: clientId, client_secret: clientSecret, }), }); } catch (error) { throw new AlephaError( `Failed to fetch access token from ${url}: ${error instanceof Error ? error.message : String(error)}`, ); } // Check HTTP status if (!response.ok) { let errorMessage = `HTTP ${response.status} ${response.statusText}`; try { const errorBody = await response.text(); errorMessage += `: ${errorBody}`; } catch { // Ignore error reading body } throw new AlephaError(`Failed to fetch access token: ${errorMessage}`); } // Parse JSON response let json: any; try { json = await response.json(); } catch (error) { throw new AlephaError( `Failed to parse access token response as JSON: ${error instanceof Error ? error.message : String(error)}`, ); } // Validate response structure if (!json.access_token || !json.expires_in) { throw new AlephaError( `Invalid access token response: missing access_token or expires_in. Response: ${JSON.stringify(json)}`, ); } cacheToken(json); return json.access_token; }; return { token, }; } return { token: async () => { const tokenFromCache = getTokenFromCache(); if (tokenFromCache) { return tokenFromCache; } const token = await options.issuer.createToken(options.user); cacheToken({ ...token, issued_at: dateTimeProvider.now().unix(), }); return token.access_token; }, }; }; export type ServiceAccountPrimitiveOptions = { gracePeriod?: number; // Grace period in seconds before token expiration } & ( | { oauth2: Oauth2ServiceAccountPrimitiveOptions; } | { issuer: IssuerPrimitive; user: UserAccount; } ); export interface Oauth2ServiceAccountPrimitiveOptions { /** * Get Token URL. */ url: string; /** * Client ID. */ clientId: string; /** * Client Secret. */ clientSecret: string; } export interface ServiceAccountPrimitive { token: () => Promise<string>; } export interface ServiceAccountStore { response?: AccessTokenResponse; }