Fix web3health check + cache secret keys
This commit is contained in:
@@ -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: []
|
||||
})
|
||||
|
||||
@@ -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 || ''
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user