/* * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ import { stringToBase64 } from "../lib/base64.js"; import { HTTPClient } from "../lib/http.js"; import { parse } from "../lib/schemas.js"; import * as components from "../models/components/index.js"; import { AfterErrorContext, AfterErrorHook, BeforeRequestContext, BeforeRequestHook, HookContext, SDKInitHook, SDKInitOptions, } from "./types.js"; type Credentials = { clientID: string; clientSecret: string; tokenURL: string | undefined; }; type Session = { credentials: Credentials; token: string; expiresAt?: number; scopes: string[]; }; export class ClientCredentialsHook implements SDKInitHook, BeforeRequestHook, AfterErrorHook { private client?: HTTPClient; private sessions: Record = {}; sdkInit(opts: SDKInitOptions): SDKInitOptions { this.client = opts.client; return opts; } async beforeRequest( hookCtx: BeforeRequestContext, request: Request, ): Promise { if (!hookCtx.oAuth2Scopes) { // OAuth2 not in use return request; } const credentials = await this.getCredentials(hookCtx); if (!credentials || Object.values(credentials).some(v => v === "")) { return request; } const sessionKey = this.getSessionKey(credentials); let session = this.sessions[sessionKey]; if ( !session || !this.hasRequiredScopes(session.scopes, hookCtx.oAuth2Scopes) || this.hasTokenExpired(session.expiresAt) ) { session = await this.doTokenRequest( hookCtx, credentials, this.getScopes(hookCtx.oAuth2Scopes, session), ); this.sessions[sessionKey] = session; } request.headers.set("Authorization", `Bearer ${session.token}`); return request; } async afterError( hookCtx: AfterErrorContext, response: Response | null, error: unknown, ): Promise<{ response: Response | null; error: unknown }> { if (!hookCtx.oAuth2Scopes) { // OAuth2 not in use return { response, error }; } if (error) { return { response, error }; } const credentials = await this.getCredentials(hookCtx); if (!credentials || Object.values(credentials).some(v => v === "")) { return { response, error }; } if (response && response?.status === 401) { const sessionKey = this.getSessionKey(credentials); delete this.sessions[sessionKey]; } return { response, error }; } private async doTokenRequest( hookCtx: HookContext, credentials: Credentials, scopes: string[], ): Promise { const formData = new URLSearchParams(); formData.append("grant_type", "client_credentials"); const headers: Record = { "Content-Type": "application/x-www-form-urlencoded", }; formData.append("client_id", credentials.clientID); formData.append("client_secret", credentials.clientSecret); if (scopes.length > 0) { formData.append("scope", scopes.join(" ")); } if (!hookCtx.baseURL?.toString()) { throw new Error("Assertion error: Base URL cannot be empty"); } const tokenURL = new URL(credentials.tokenURL ?? "", hookCtx.baseURL); const request = new Request(tokenURL, { method: "POST", headers, body: formData, }); const res = await this.client?.request(request); if (!res) { throw new Error("Failed to fetch token"); } if (res.status < 200 || res.status >= 300) { throw new Error("Unexpected status code"); } const data = await res.json(); if (!data || typeof data !== "object") { throw new Error("Unexpected response format"); } if (data.token_type !== "Bearer") { throw new Error("Unexpected token type"); } let expiresAt: number | undefined; if (data.expires_in) { expiresAt = Date.now() + data.expires_in * 1000; } const sess: Session = { credentials, token: data.access_token, scopes, }; if (expiresAt !== undefined) { sess.expiresAt = expiresAt; } return sess; } private async getCredentials( hookCtx: HookContext, ): Promise { const source = hookCtx.securitySource; if (!source) { return null; } let security = source; if (typeof source === "function") { security = await source(); } return this.getCredentialsGlobal(security); } private async getCredentialsGlobal( security: unknown, ): Promise { const out = parse( security, (val) => components.Security$outboundSchema.parse(val), "unexpected security type", ); return { clientID: out?.clientID ?? "", clientSecret: out?.clientSecret ?? "", tokenURL: out?.tokenURL ?? "", }; } private getSessionKey(credentials: Credentials): string { const key = `${credentials.clientID}:${credentials.clientSecret}`; return stringToBase64(key); } private hasRequiredScopes( scopes: string[], requiredScopes: string[], ): boolean { return requiredScopes.every((scope) => scopes.includes(scope)); } private getScopes(requiredScopes: string[], sess?: Session): string[] { return [...new Set((sess?.scopes ?? []).concat(requiredScopes))]; } private hasTokenExpired(expiresAt?: number): boolean { return !expiresAt || Date.now() + 60000 > expiresAt; } }