import crypto from 'node:crypto'; import { type Request, type RequestHandler } from 'express'; import { type FastifyReply, type FastifyRequest } from 'fastify'; import jwt from 'jsonwebtoken'; import { JwksClient } from 'jwks-rsa'; import { type SecretEnv, type Resource, inverseEnvMap, type MiauClientToken, type HasPermissionResponse } from '@eduzz/miau-types'; import { cacheableFetch } from './functions'; import { miauHook, miauMiddleware, type RequestAugmentation } from './middleware'; type MiauClientConfig = { apiUrl: string; appSecret: string }; type OAuthTokenSuccessResponse = { access_token: string; token_type: 'Bearer'; expires_in: number; }; type OAuthTokenErrorResponse = { error: string; error_description: string; message?: string; }; type OAuthTokenResponse = OAuthTokenSuccessResponse | OAuthTokenErrorResponse; export class MiauClient { private config: MiauClientConfig; private jwtToken: string | undefined; private jwksClient: JwksClient | undefined; private basicAuthToken: string; constructor(config: MiauClientConfig) { if (!config.apiUrl || !config.appSecret) { throw new Error('Invalid MiauClient configuration. Please provide apiUrl and appSecret.'); } this.config = config; const apiKey = config.appSecret.substring(7, 32); const hashedSecret = crypto.createHash('sha256').update(config.appSecret).digest('hex'); this.basicAuthToken = Buffer.from(`${apiKey}:${hashedSecret}`).toString('base64'); } public getEnvironment(): SecretEnv { const envValue = this.config.appSecret.substring(5, 6); const environment = inverseEnvMap[envValue as keyof typeof inverseEnvMap]; if (!environment) { throw new Error('Invalid environment in appSecret.'); } return environment; } public async getPublicKey(kid: string) { if (!this.jwksClient) { this.jwksClient = new JwksClient({ jwksUri: this.getJwksUrl(), cache: true }); } const signingKey = await this.jwksClient.getSigningKey(kid); return signingKey.getPublicKey(); } public async getToken() { if (this.jwtToken) { const { exp } = jwt.decode(this.jwtToken) as { exp: number }; const ONE_MINUTE_FROM_NOW = Math.floor(Date.now() / 1000) + 60; if (exp > ONE_MINUTE_FROM_NOW) { return this.jwtToken; } } const response = await fetch(this.getOAuthTokenUrl(), { headers: { 'Authorization': `Basic ${this.basicAuthToken}`, 'Content-Type': 'application/json' } }); const data = (await response.json()) as OAuthTokenResponse; if (response.status !== 200) { const errorData = data as OAuthTokenErrorResponse; const errorMessage = errorData.message || errorData.error_description || `OAuth error: ${errorData.error}` || 'Failed to fetch JWT token'; throw new Error(errorMessage); } const successData = data as OAuthTokenSuccessResponse; this.jwtToken = successData.access_token; return this.jwtToken; } public getTokenData = async () => { const token = await this.getToken(); if (!token) { throw new Error('Token is undefined'); } return jwt.decode(token) as MiauClientToken; }; public middleware>(config?: { requestAugmentation?: RequestAugmentation; fallbackMiddleware?: RequestHandler; }): RequestHandler { return miauMiddleware(this, config?.requestAugmentation, config?.fallbackMiddleware); } public hook>(config?: { requestAugmentation?: RequestAugmentation; fallbackMiddleware?: (request: FastifyRequest, reply: FastifyReply) => void; }): (request: FastifyRequest, reply: FastifyReply) => void { return miauHook(this, config?.requestAugmentation, config?.fallbackMiddleware); } public async hasPermission(sourceAppId: string, resource: Resource) { const data = { sourceAppId, resource }; return cacheableFetch(`${this.getHasPermissionUrl()}`, { headers: { 'Authorization': `Basic ${this.basicAuthToken}`, 'Content-Type': 'application/json' }, method: 'POST', body: JSON.stringify(data) }); } public async verify(token: string, publicKey: string): Promise { return jwt.verify(token, publicKey, { algorithms: ['RS256'] }) as MiauClientToken; } private getJwksUrl = () => { return `${this.config.apiUrl}/v1/jwks.json`; }; private getHasPermissionUrl = () => { return `${this.config.apiUrl}/v1/has-permission`; }; private getOAuthTokenUrl = () => { return `${this.config.apiUrl}/v1/oauth/token`; }; }