From 7fee636fc420aceb52ef68b60ee409173f03b3d8 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Sat, 1 Nov 2025 13:10:19 +0700 Subject: [PATCH] Fix web3health check + cache secret keys --- .../src/plugins/custom/privy-secrets.ts | 61 ++++-- .../src/plugins/custom/privy.ts | 27 ++- src/Managing.Web3Proxy/src/routes/home.ts | 192 ++++++++++++++---- 3 files changed, 218 insertions(+), 62 deletions(-) diff --git a/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts b/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts index 65cc64f6..a5beeef3 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts @@ -13,19 +13,28 @@ declare module 'fastify' { } /** - * Loads Privy secrets from Infisical in production, or from env vars/files in non-production + * Loads Privy secrets ONCE at application startup from Infisical (production) or env vars/files (non-production). + * Secrets are cached in memory via Fastify decorator and never reloaded during runtime. + * This ensures secrets are only fetched once from Infisical at startup, not on every Privy client creation. */ export default fp(async function (fastify) { + // Guard: Fastify-plugin ensures this runs only once, but add explicit check as safety + if ((fastify as any).privySecrets) { + fastify.log.warn('privySecrets already decorated - skipping reload (this should not happen)') + return + } + const isProd = process.env.NODE_ENV === 'production' - fastify.log.info({ isProd, nodeEnv: process.env.NODE_ENV }, 'Loading Privy secrets') + fastify.log.info({ isProd, nodeEnv: process.env.NODE_ENV }, 'Loading Privy secrets at startup (one-time only)') let appId: string = '' let appSecret: string = '' let authKey: string = '' + let infisicalClient: InfisicalSDK | null = null if (isProd) { - // In production, use Infisical SDK + // In production, use Infisical SDK - this runs ONCE at startup try { const infisicalSiteUrl = process.env.INFISICAL_SITE_URL || 'https://app.infisical.com' const infisicalClientId = process.env.INFISICAL_CLIENT_ID @@ -41,22 +50,23 @@ export default fp(async function (fastify) { siteUrl: infisicalSiteUrl, projectId: infisicalProjectId, environment: infisicalEnvironment - }, 'Initializing Infisical SDK') + }, 'Initializing Infisical SDK (startup only)') - const client = new InfisicalSDK({ + // Create Infisical client - used once at startup + infisicalClient = new InfisicalSDK({ siteUrl: infisicalSiteUrl }) - // Authenticate with Infisical - await client.auth().universalAuth.login({ + // Authenticate with Infisical - done once at startup + await infisicalClient.auth().universalAuth.login({ clientId: infisicalClientId, clientSecret: infisicalClientSecret }) - fastify.log.info('Authenticated with Infisical successfully') + fastify.log.info('Authenticated with Infisical successfully (startup only)') - // Fetch all secrets for the project - const allSecrets = await client.secrets().listSecrets({ + // Fetch all secrets for the project - done ONCE at startup + const allSecrets = await infisicalClient.secrets().listSecrets({ environment: infisicalEnvironment, projectId: infisicalProjectId, viewSecretValue: true @@ -64,9 +74,9 @@ export default fp(async function (fastify) { fastify.log.info({ secretCount: allSecrets.secrets?.length || 0 - }, 'Fetched secrets from Infisical') + }, 'Fetched secrets from Infisical (startup only)') - // Extract Privy secrets + // Extract Privy secrets and store in memory const privyAppIdSecret = allSecrets.secrets?.find(s => s.secretKey === 'PRIVY_APP_ID') const privyAppSecretSecret = allSecrets.secrets?.find(s => s.secretKey === 'PRIVY_APP_SECRET') const privyAuthKeySecret = allSecrets.secrets?.find(s => s.secretKey === 'PRIVY_AUTHORIZATION_KEY') @@ -82,13 +92,18 @@ export default fp(async function (fastify) { appIdFound: !!privyAppIdSecret, appSecretFound: !!privyAppSecretSecret, authKeyFound: !!privyAuthKeySecret - }, 'Privy secrets loaded from Infisical') + }, 'Privy secrets loaded from Infisical and cached in memory (startup only)') + + // Note: Infisical SDK doesn't expose explicit cleanup methods + // The client will be garbage collected after secrets are extracted + // No need to keep the client instance - secrets are now in memory + infisicalClient = null } catch (error) { fastify.log.error({ error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined - }, 'Failed to load secrets from Infisical') + }, 'Failed to load secrets from Infisical at startup') // Fallback to environment variables appId = process.env.PRIVY_APP_ID || '' @@ -96,9 +111,12 @@ export default fp(async function (fastify) { authKey = process.env.PRIVY_AUTHORIZATION_KEY || '' fastify.log.warn('Falling back to environment variables for Privy secrets') + } finally { + // Ensure client is cleaned up + infisicalClient = null } } else { - // In non-production, use env vars or file paths + // In non-production, use env vars or file paths - loaded once at startup const readMaybeFile = (envKey: string, fileKey: string): string | undefined => { const filePath = process.env[fileKey] if (filePath && fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf8').trim() @@ -146,6 +164,8 @@ export default fp(async function (fastify) { return // Continue without throwing } + // Decorate Fastify instance with secrets - these are cached in memory for the lifetime of the application + // This decoration happens ONCE at startup, and secrets are reused from memory for all Privy client creations fastify.decorate('privySecrets', { appId, appSecret, @@ -155,6 +175,11 @@ export default fp(async function (fastify) { fastify.log.info({ appId: appId.substring(0, 10) + '...', appSecret: appSecret.substring(0, 10) + '...', - authKey: authKey.substring(0, 20) + '...' - }, '✅ Privy secrets decorated on Fastify instance successfully') -}, { name: 'privy-secrets' }) + authKey: authKey.substring(0, 20) + '...', + note: 'Secrets cached in memory - Infisical will not be called again during runtime' + }, '✅ Privy secrets loaded at startup and cached in memory') +}, { + name: 'privy-secrets', + // Ensure plugin runs only once per Fastify instance + dependencies: [] +}) diff --git a/src/Managing.Web3Proxy/src/plugins/custom/privy.ts b/src/Managing.Web3Proxy/src/plugins/custom/privy.ts index 70a0b0a4..4fb18a1a 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/privy.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/privy.ts @@ -51,14 +51,18 @@ declare module 'fastify' { } /** - * Returns an initialized PrivyClient instance with configuration from secrets + * Returns an initialized PrivyClient instance with configuration from cached secrets. + * Secrets are loaded ONCE at startup from Infisical (production) or env vars (non-production) + * and cached in memory via Fastify decorator. This function only reads from cache, never fetches from Infisical. + * @param fastify Optional Fastify instance to access cached secrets from decorator * @returns The configured PrivyClient instance */ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => { - // Try multiple sources in order of preference: - // 1. Fastify decorator (populated by privy-secrets plugin - Infisical in prod, env in dev) + // Read from cached secrets in memory (loaded once at startup): + // 1. Fastify decorator (populated by privy-secrets plugin at startup - Infisical in prod, env in dev) // 2. Fastify config (from env plugin) // 3. Environment variables (fallback) + // Note: In production, secrets come from Infisical and are cached in fastify.privySecrets at startup const decorated = (fastify as any)?.privySecrets as { appId: string; appSecret: string; authKey: string } | undefined @@ -92,17 +96,18 @@ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => { }; /** - * Authentication function for Privy API calls + * Authentication function for Privy API calls. + * Reads cached secrets from memory (loaded once at startup from Infisical in production). * @param url The URL for the API endpoint * @param body The request body - * @param fastify Optional Fastify instance to get secrets from decorator + * @param fastify Optional Fastify instance to get cached secrets from decorator * @returns The authorization signature */ export async function getAuthorizationSignature( {url, body}: {url: string; body: object}, fastify?: FastifyInstance ) { - // Get app ID from Fastify decorator or env + // Get app ID from cached secrets (loaded once at startup) or env fallback const decorated = (fastify as any)?.privySecrets as { appId: string; authKey: string } | undefined const appId = decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID || '' @@ -121,10 +126,10 @@ export async function getAuthorizationSignature( const serializedPayload = canonicalize(payload) as string; const serializedPayloadBuffer = Buffer.from(serializedPayload); - // Resolve authorization key: from Fastify decorator (Infisical in prod), file/env in non-production + // Resolve authorization key from cached secrets (loaded once at startup from Infisical in production) let resolvedKey: string | undefined - // Try Fastify decorator first (loaded from Infisical in production) + // Try Fastify decorator first (secrets cached in memory from Infisical at startup in production) if (decorated?.authKey) { resolvedKey = decorated.authKey } else if (fastify?.config?.PRIVY_AUTHORIZATION_KEY) { @@ -154,10 +159,12 @@ export async function getAuthorizationSignature( /** * Makes a request to the Privy API with proper authentication and headers. + * Reads cached secrets from memory (loaded once at startup from Infisical in production). * @param url - The full URL for the API endpoint. * @param body - The request body (optional for GET requests). * @param requiresAuth - Whether the request requires authentication. * @param method - The HTTP method (defaults to POST). + * @param fastify - Optional Fastify instance to access cached secrets from decorator * @returns The response data. */ export const makePrivyRequest = async ( @@ -168,10 +175,10 @@ export const makePrivyRequest = async ( fastify?: FastifyInstance ): Promise => { try { - // Resolve secrets: from Fastify decorator (Infisical in prod), env in non-production + // Resolve secrets from cached memory (loaded once at startup from Infisical in production) const decorated = (fastify as any)?.privySecrets as { appId: string; appSecret: string } | undefined - // Try Fastify decorator first (loaded from Infisical in production) + // Try Fastify decorator first (secrets cached in memory from Infisical at startup in production) const appId = decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID || '' const appSecret = decorated?.appSecret || fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET || '' diff --git a/src/Managing.Web3Proxy/src/routes/home.ts b/src/Managing.Web3Proxy/src/routes/home.ts index 92109c4e..38acf67a 100644 --- a/src/Managing.Web3Proxy/src/routes/home.ts +++ b/src/Managing.Web3Proxy/src/routes/home.ts @@ -34,28 +34,20 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { fastify.get('/health', { schema: { tags: ['Health'], - description: 'Enhanced health check endpoint that verifies API, Privy, and GMX connectivity', + description: 'Enhanced health check endpoint that verifies API, Privy, Infisical (production), GMX, and Redis connectivity', response: { 200: Type.Object({ status: Type.String(), timestamp: Type.String(), version: Type.String(), - checks: Type.Object({ - privy: Type.Object({ - status: Type.String(), - message: Type.String() - }), - gmx: Type.Object({ - status: Type.String(), - message: Type.String(), - data: Type.Optional(Type.Any()) - }), - redis: Type.Object({ + checks: Type.Record( + Type.String(), + Type.Object({ status: Type.String(), message: Type.String(), data: Type.Optional(Type.Any()) }) - }) + ) }), 500: Type.Object({ success: Type.Boolean(), @@ -66,12 +58,19 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { }, async function (request, reply) { try { console.log('Checking health...') - const checks = { + const isProd = process.env.NODE_ENV === 'production' + + const checks: Record = { privy: await checkPrivy(fastify), gmx: await checkGmx(), redis: await checkRedis() } + // In production, add Infisical health check + if (isProd) { + checks.infisical = await checkInfisical(fastify) + } + // If any check failed, set status to degraded const overallStatus = Object.values(checks).some(check => check.status !== 'healthy') ? 'degraded' @@ -91,40 +90,165 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { // Helper function to check Privy connectivity and configuration async function checkPrivy(fastify) { try { - // Try to initialize the Privy client - this will validate env vars and connectivity - const privy = getPrivyClient(fastify); + // Check if secrets are loaded via Fastify decorator (from Infisical in prod) + const privySecrets = (fastify as any)?.privySecrets as { appId: string; appSecret: string; authKey: string } | undefined + const isProd = process.env.NODE_ENV === 'production' - // Check if the necessary configuration is available - const hasPrivyAppId = !!process.env.PRIVY_APP_ID; - const hasPrivySecret = !!process.env.PRIVY_APP_SECRET; - const hasPrivyAuthKey = !!process.env.PRIVY_AUTHORIZATION_KEY; - - // Get the client status - const allConfigPresent = hasPrivyAppId && hasPrivySecret && hasPrivyAuthKey; - - if (!allConfigPresent) { - return { - status: 'degraded', - message: 'Privy configuration incomplete: ' + - (!hasPrivyAppId ? 'PRIVY_APP_ID ' : '') + - (!hasPrivySecret ? 'PRIVY_APP_SECRET ' : '') + - (!hasPrivyAuthKey ? 'PRIVY_AUTHORIZATION_KEY ' : '') - }; + if (isProd) { + // In production, verify secrets are loaded from Infisical + if (!privySecrets) { + return { + status: 'unhealthy', + message: 'Privy secrets not loaded from Infisical - privySecrets decorator not found', + data: { + source: 'infisical', + secretsDecorated: false + } + } + } + + const hasAppId = !!privySecrets.appId + const hasAppSecret = !!privySecrets.appSecret + const hasAuthKey = !!privySecrets.authKey + + if (!hasAppId || !hasAppSecret || !hasAuthKey) { + return { + status: 'unhealthy', + message: 'Privy secrets incomplete from Infisical', + data: { + source: 'infisical', + appId: hasAppId, + appSecret: hasAppSecret, + authKey: hasAuthKey + } + } + } + } else { + // In non-production, check environment variables + const hasPrivyAppId = !!process.env.PRIVY_APP_ID + const hasPrivySecret = !!process.env.PRIVY_APP_SECRET + const hasPrivyAuthKey = !!process.env.PRIVY_AUTHORIZATION_KEY + + if (!hasPrivyAppId || !hasPrivySecret || !hasPrivyAuthKey) { + return { + status: 'degraded', + message: 'Privy configuration incomplete from environment variables', + data: { + source: 'environment', + appId: hasPrivyAppId, + appSecret: hasPrivySecret, + authKey: hasPrivyAuthKey + } + } + } } + // Try to initialize the Privy client - this will validate secrets and connectivity + const privy = getPrivyClient(fastify); + const appSettings = await privy.getAppSettings() + // If we got this far, the Privy client was successfully initialized return { status: 'healthy', - message: 'Privy client successfully initialized' + message: 'Privy client successfully initialized', + data: { + source: isProd ? 'infisical' : 'environment', + appId: isProd ? privySecrets?.appId.substring(0, 10) + '...' : process.env.PRIVY_APP_ID?.substring(0, 10) + '...', + privyAppSettingsId: appSettings.id + } }; } catch (error) { return { status: 'unhealthy', - message: `Privy client initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}` + message: `Privy client initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + data: { + errorType: error instanceof Error ? error.constructor.name : 'Unknown' + } }; } } + // Helper function to check Infisical connectivity and secret loading + async function checkInfisical(fastify) { + try { + const infisicalClientId = process.env.INFISICAL_CLIENT_ID + const infisicalClientSecret = process.env.INFISICAL_CLIENT_SECRET + const infisicalProjectId = process.env.INFISICAL_PROJECT_ID + const infisicalEnvironment = process.env.INFISICAL_ENVIRONMENT || 'prod' + const infisicalSiteUrl = process.env.INFISICAL_SITE_URL || 'https://app.infisical.com' + + // Check if required Infisical environment variables are set + if (!infisicalClientId || !infisicalClientSecret || !infisicalProjectId) { + return { + status: 'unhealthy', + message: 'Infisical configuration incomplete - missing required environment variables', + data: { + hasClientId: !!infisicalClientId, + hasClientSecret: !!infisicalClientSecret, + hasProjectId: !!infisicalProjectId, + environment: infisicalEnvironment, + siteUrl: infisicalSiteUrl + } + } + } + + // Check if secrets are loaded in Fastify decorator + const privySecrets = (fastify as any)?.privySecrets as { appId: string; appSecret: string; authKey: string } | undefined + + if (!privySecrets) { + return { + status: 'unhealthy', + message: 'Infisical secrets not loaded - privySecrets decorator not found', + data: { + projectId: infisicalProjectId, + environment: infisicalEnvironment + } + } + } + + // Verify all three secrets are present + const hasAllSecrets = !!(privySecrets.appId && privySecrets.appSecret && privySecrets.authKey) + + if (!hasAllSecrets) { + return { + status: 'unhealthy', + message: 'Infisical secrets incomplete', + data: { + projectId: infisicalProjectId, + environment: infisicalEnvironment, + appId: !!privySecrets.appId, + appSecret: !!privySecrets.appSecret, + authKey: !!privySecrets.authKey + } + } + } + + // All checks passed + return { + status: 'healthy', + message: 'Infisical secrets successfully loaded', + data: { + projectId: infisicalProjectId, + environment: infisicalEnvironment, + siteUrl: infisicalSiteUrl, + secretsLoaded: { + PRIVY_APP_ID: !!privySecrets.appId, + PRIVY_APP_SECRET: !!privySecrets.appSecret, + PRIVY_AUTHORIZATION_KEY: !!privySecrets.authKey + } + } + } + } catch (error) { + return { + status: 'unhealthy', + message: `Infisical check failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + data: { + errorType: error instanceof Error ? error.constructor.name : 'Unknown' + } + } + } + } + // Helper function to check GMX connectivity using the GMX SDK async function checkGmx() { try {