Fix web3health check + cache secret keys

This commit is contained in:
2025-11-01 13:10:19 +07:00
parent ab37da2cca
commit 7fee636fc4
3 changed files with 218 additions and 62 deletions

View File

@@ -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: []
})

View File

@@ -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 <T>(
@@ -168,10 +175,10 @@ export const makePrivyRequest = async <T>(
fastify?: FastifyInstance
): Promise<T> => {
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 || ''

View File

@@ -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<string, {status: string; message: string; data?: any}> = {
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;
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
}
}
}
// Get the client status
const allConfigPresent = hasPrivyAppId && hasPrivySecret && hasPrivyAuthKey;
const hasAppId = !!privySecrets.appId
const hasAppSecret = !!privySecrets.appSecret
const hasAuthKey = !!privySecrets.authKey
if (!allConfigPresent) {
return {
status: 'degraded',
message: 'Privy configuration incomplete: ' +
(!hasPrivyAppId ? 'PRIVY_APP_ID ' : '') +
(!hasPrivySecret ? 'PRIVY_APP_SECRET ' : '') +
(!hasPrivyAuthKey ? 'PRIVY_AUTHORIZATION_KEY ' : '')
};
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 {