Use infisical for secrets

This commit is contained in:
2025-11-01 12:52:06 +07:00
parent cf855e37b9
commit ab37da2cca
6 changed files with 2094 additions and 180 deletions

View File

@@ -1,5 +1,4 @@
{ {
"schemaVersion": 2, "schemaVersion": 2,
"dockerfilePath": "./src/Managing.Web3Proxy/Dockerfile-web3proxy", "dockerfilePath": "./src/Managing.Web3Proxy/Dockerfile-web3proxy"
"preDeployFunction": "function(captainAppObj, dockerUpdateObject) {\n try {\n console.log('preDeployFunction executing...');\n \n // Ensure TaskTemplate exists\n if (!dockerUpdateObject.TaskTemplate) {\n dockerUpdateObject.TaskTemplate = {};\n }\n \n // Ensure ContainerSpec exists\n if (!dockerUpdateObject.TaskTemplate.ContainerSpec) {\n dockerUpdateObject.TaskTemplate.ContainerSpec = {};\n }\n \n // Initialize Secrets array if it doesn't exist\n if (!dockerUpdateObject.TaskTemplate.ContainerSpec.Secrets) {\n dockerUpdateObject.TaskTemplate.ContainerSpec.Secrets = [];\n }\n \n // Add secrets configuration\n dockerUpdateObject.TaskTemplate.ContainerSpec.Secrets = [\n {\n File: {\n Name: 'PRIVY_APP_ID',\n UID: '0',\n GID: '0',\n Mode: 292\n },\n SecretID: '0r43i7pryk5d2fu0q9vyf46km',\n SecretName: 'privy_app_id'\n },\n {\n File: {\n Name: 'PRIVY_APP_SECRET',\n UID: '0',\n GID: '0',\n Mode: 292\n },\n SecretID: '0z6mjh58drzcrhk2kfbzt1f14',\n SecretName: 'privy_app_secret'\n },\n {\n File: {\n Name: 'PRIVY_AUTHORIZATION_KEY',\n UID: '0',\n GID: '0',\n Mode: 292\n },\n SecretID: 'ieg5bklu1t0otxrp69j4pekvj',\n SecretName: 'privy_auth_key'\n }\n ];\n \n console.log('Secrets configured:', dockerUpdateObject.TaskTemplate.ContainerSpec.Secrets.length, 'secrets');\n \n return Promise.resolve();\n } catch (error) {\n console.error('Error in preDeployFunction:', error);\n throw error;\n }\n}"
} }

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,7 @@
"@fastify/swagger-ui": "^5.0.1", "@fastify/swagger-ui": "^5.0.1",
"@fastify/type-provider-typebox": "^5.0.0", "@fastify/type-provider-typebox": "^5.0.0",
"@fastify/under-pressure": "^9.0.1", "@fastify/under-pressure": "^9.0.1",
"@infisical/sdk": "^4.0.6",
"@privy-io/server-auth": "^1.18.12", "@privy-io/server-auth": "^1.18.12",
"@sentry/node": "^8.55.0", "@sentry/node": "^8.55.0",
"@sinclair/typebox": "^0.34.11", "@sinclair/typebox": "^0.34.11",

View File

@@ -1,5 +1,6 @@
import fp from 'fastify-plugin' import fp from 'fastify-plugin'
import fs from 'fs' import fs from 'fs'
import {InfisicalSDK} from '@infisical/sdk'
declare module 'fastify' { declare module 'fastify' {
interface FastifyInstance { interface FastifyInstance {
@@ -11,101 +12,91 @@ declare module 'fastify' {
} }
} }
// Docker secrets are mounted at /run/secrets/ by default in CapRover /**
const readSecretFile = (secretName: string, logger?: any): string | undefined => { * Loads Privy secrets from Infisical in production, or from env vars/files in non-production
const secretPath = `/run/secrets/${secretName}` */
logger?.debug({ secretPath, exists: fs.existsSync(secretPath) }, `Checking secret file: ${secretName}`)
if (fs.existsSync(secretPath)) {
try {
const content = fs.readFileSync(secretPath, 'utf8').trim()
logger?.info({ secretName, path: secretPath, length: content.length }, `Successfully read secret: ${secretName}`)
return content
} catch (error) {
logger?.error({ error, secretPath }, `Failed to read secret file ${secretPath}`)
return undefined
}
} else {
logger?.warn({ secretPath }, `Secret file does not exist: ${secretPath}`)
}
return undefined
}
export default fp(async function (fastify) { export default fp(async function (fastify) {
const isProd = process.env.NODE_ENV === 'production' 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')
// Debug: List all files in /run/secrets/ directory let appId: string = ''
const secretsDir = '/run/secrets' let appSecret: string = ''
let availableFiles: string[] = [] let authKey: string = ''
if (fs.existsSync(secretsDir)) {
try {
availableFiles = fs.readdirSync(secretsDir)
fastify.log.info({ files: availableFiles, dir: secretsDir, count: availableFiles.length }, 'Files found in /run/secrets/')
// Also log full paths and their sizes
availableFiles.forEach(file => {
const fullPath = `${secretsDir}/${file}`
try {
const stats = fs.statSync(fullPath)
fastify.log.debug({ file, path: fullPath, size: stats.size, isFile: stats.isFile() }, `Secret file details: ${file}`)
} catch (err) {
fastify.log.warn({ file, path: fullPath, error: err }, `Could not stat secret file: ${file}`)
}
})
} catch (error) {
fastify.log.warn({ error }, 'Could not list /run/secrets/ directory')
}
} else {
fastify.log.error({ dir: secretsDir }, '/run/secrets/ directory does not exist - Docker secrets may not be mounted')
}
let appId: string
let appSecret: string
let authKey: string
if (isProd) { if (isProd) {
// In production, read from Docker secrets (mounted files) // In production, use Infisical SDK
// Try exact names first try {
appId = readSecretFile('PRIVY_APP_ID', fastify.log) || process.env.PRIVY_APP_ID || '' const infisicalSiteUrl = process.env.INFISICAL_SITE_URL || 'https://app.infisical.com'
appSecret = readSecretFile('PRIVY_APP_SECRET', fastify.log) || process.env.PRIVY_APP_SECRET || '' const infisicalClientId = process.env.INFISICAL_CLIENT_ID
authKey = readSecretFile('PRIVY_AUTHORIZATION_KEY', fastify.log) || process.env.PRIVY_AUTHORIZATION_KEY || '' const infisicalClientSecret = process.env.INFISICAL_CLIENT_SECRET
const infisicalProjectId = process.env.INFISICAL_PROJECT_ID
const infisicalEnvironment = process.env.INFISICAL_ENVIRONMENT || 'prod'
// If not found, try alternative names (maybe they're mounted with SecretName instead of File.Name) if (!infisicalClientId || !infisicalClientSecret || !infisicalProjectId) {
if (!appId && availableFiles.length > 0) { throw new Error('Infisical credentials missing: INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, and INFISICAL_PROJECT_ID are required in production')
const appIdFile = availableFiles.find(f => f.toLowerCase().includes('app_id') || f.toLowerCase().includes('appid'))
if (appIdFile) {
fastify.log.info({ foundFile: appIdFile, trying: 'PRIVY_APP_ID' }, 'Trying alternative file name for PRIVY_APP_ID')
appId = readSecretFile(appIdFile, fastify.log) || ''
}
} }
if (!appSecret && availableFiles.length > 0) { fastify.log.info({
const appSecretFile = availableFiles.find(f => f.toLowerCase().includes('app_secret') || f.toLowerCase().includes('appsecret')) siteUrl: infisicalSiteUrl,
if (appSecretFile) { projectId: infisicalProjectId,
fastify.log.info({ foundFile: appSecretFile, trying: 'PRIVY_APP_SECRET' }, 'Trying alternative file name for PRIVY_APP_SECRET') environment: infisicalEnvironment
appSecret = readSecretFile(appSecretFile, fastify.log) || '' }, 'Initializing Infisical SDK')
}
}
if (!authKey && availableFiles.length > 0) { const client = new InfisicalSDK({
const authKeyFile = availableFiles.find(f => f.toLowerCase().includes('auth_key') || f.toLowerCase().includes('authkey') || f.toLowerCase().includes('authorization')) siteUrl: infisicalSiteUrl
if (authKeyFile) { })
fastify.log.info({ foundFile: authKeyFile, trying: 'PRIVY_AUTHORIZATION_KEY' }, 'Trying alternative file name for PRIVY_AUTHORIZATION_KEY')
authKey = readSecretFile(authKeyFile, fastify.log) || '' // Authenticate with Infisical
} await client.auth().universalAuth.login({
} clientId: infisicalClientId,
clientSecret: infisicalClientSecret
})
fastify.log.info('Authenticated with Infisical successfully')
// Fetch all secrets for the project
const allSecrets = await client.secrets().listSecrets({
environment: infisicalEnvironment,
projectId: infisicalProjectId,
viewSecretValue: true
})
fastify.log.info({
secretCount: allSecrets.secrets?.length || 0
}, 'Fetched secrets from Infisical')
// Extract Privy secrets
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')
appId = privyAppIdSecret?.secretValue || process.env.PRIVY_APP_ID || ''
appSecret = privyAppSecretSecret?.secretValue || process.env.PRIVY_APP_SECRET || ''
authKey = privyAuthKeySecret?.secretValue || process.env.PRIVY_AUTHORIZATION_KEY || ''
fastify.log.info({ fastify.log.info({
appId: !!appId, appId: !!appId,
appSecret: !!appSecret, appSecret: !!appSecret,
authKey: !!authKey, authKey: !!authKey,
appIdLength: appId.length, appIdFound: !!privyAppIdSecret,
appSecretLength: appSecret.length, appSecretFound: !!privyAppSecretSecret,
authKeyLength: authKey.length, authKeyFound: !!privyAuthKeySecret
availableSecretFiles: availableFiles }, 'Privy secrets loaded from Infisical')
}, 'Privy secrets loaded from Docker secrets')
} 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')
// Fallback to environment variables
appId = process.env.PRIVY_APP_ID || ''
appSecret = process.env.PRIVY_APP_SECRET || ''
authKey = process.env.PRIVY_AUTHORIZATION_KEY || ''
fastify.log.warn('Falling back to environment variables for Privy secrets')
}
} else { } else {
// In non-production, use env vars or file paths // In non-production, use env vars or file paths
const readMaybeFile = (envKey: string, fileKey: string): string | undefined => { const readMaybeFile = (envKey: string, fileKey: string): string | undefined => {
@@ -127,9 +118,7 @@ export default fp(async function (fastify) {
appSecret: !!appSecret, appSecret: !!appSecret,
authKey: !!authKey, authKey: !!authKey,
isProd, isProd,
nodeEnv: process.env.NODE_ENV, nodeEnv: process.env.NODE_ENV
availableSecretFiles: availableFiles,
secretsDirExists: fs.existsSync(secretsDir)
}, '⚠️ WARNING: Privy secrets not fully resolved at plugin load - app will continue but Privy operations will fail') }, '⚠️ WARNING: Privy secrets not fully resolved at plugin load - app will continue but Privy operations will fail')
// Still decorate with empty strings so the app doesn't crash // Still decorate with empty strings so the app doesn't crash
@@ -142,11 +131,15 @@ export default fp(async function (fastify) {
fastify.log.error({ fastify.log.error({
message: 'Please check:', message: 'Please check:',
checks: [ checks: isProd ? [
'1. Docker secrets are created: docker secret ls', '1. INFISICAL_CLIENT_ID environment variable is set',
'2. preDeployFunction in captain-definition is executing', '2. INFISICAL_CLIENT_SECRET environment variable is set',
'3. Secret IDs in captain-definition match actual secret IDs', '3. INFISICAL_PROJECT_ID environment variable is set',
'4. Container has access to /run/secrets/ directory' '4. Secrets exist in Infisical with keys: PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_AUTHORIZATION_KEY',
'5. Machine Identity has access to the project and environment'
] : [
'1. Environment variables PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_AUTHORIZATION_KEY are set',
'2. Or file paths are set via PRIVY_APP_ID_FILE, PRIVY_APP_SECRET_FILE, PRIVY_AUTHORIZATION_KEY_FILE'
] ]
}, 'Debugging steps for missing secrets') }, 'Debugging steps for missing secrets')

View File

@@ -51,53 +51,20 @@ declare module 'fastify' {
} }
/** /**
* Reads a secret from Docker secrets mount point (production only) * Returns an initialized PrivyClient instance with configuration from secrets
*/
const readDockerSecret = (secretName: string): string | undefined => {
if (process.env.NODE_ENV !== 'production') return undefined
const secretPath = `/run/secrets/${secretName}`
if (fs.existsSync(secretPath)) {
try {
return fs.readFileSync(secretPath, 'utf8').trim()
} catch (error) {
console.error(`Failed to read Docker secret ${secretPath}:`, error)
return undefined
}
}
return undefined
}
/**
* Returns an initialized PrivyClient instance with configuration from environment variables
* @returns The configured PrivyClient instance * @returns The configured PrivyClient instance
*/ */
export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => { export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
const isProd = process.env.NODE_ENV === 'production'
// Try multiple sources in order of preference: // Try multiple sources in order of preference:
// 1. Fastify decorator (populated by privy-secrets plugin) // 1. Fastify decorator (populated by privy-secrets plugin - Infisical in prod, env in dev)
// 2. Fastify config (from env plugin) // 2. Fastify config (from env plugin)
// 3. Docker secrets (production only) // 3. Environment variables (fallback)
// 4. Environment variables
const decorated = (fastify as any)?.privySecrets as { appId: string; appSecret: string; authKey: string } | undefined const decorated = (fastify as any)?.privySecrets as { appId: string; appSecret: string; authKey: string } | undefined
let appId = decorated?.appId || fastify?.config?.PRIVY_APP_ID const appId = decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID
let appSecret = decorated?.appSecret || fastify?.config?.PRIVY_APP_SECRET const appSecret = decorated?.appSecret || fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET
let authKey = decorated?.authKey || fastify?.config?.PRIVY_AUTHORIZATION_KEY const authKey = decorated?.authKey || fastify?.config?.PRIVY_AUTHORIZATION_KEY || process.env.PRIVY_AUTHORIZATION_KEY
// In production, fall back to Docker secrets if not found in decorator/config
if (isProd) {
if (!appId) appId = readDockerSecret('PRIVY_APP_ID')
if (!appSecret) appSecret = readDockerSecret('PRIVY_APP_SECRET')
if (!authKey) authKey = readDockerSecret('PRIVY_AUTHORIZATION_KEY')
}
// Final fallback to environment variables
appId = appId || process.env.PRIVY_APP_ID
appSecret = appSecret || process.env.PRIVY_APP_SECRET
authKey = authKey || process.env.PRIVY_AUTHORIZATION_KEY
if (!appId || !appSecret || !authKey) { if (!appId || !appSecret || !authKey) {
console.warn('Missing Privy environment variables:'); console.warn('Missing Privy environment variables:');
@@ -108,18 +75,9 @@ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
console.warn('Fastify privySecrets decorated:', !!decorated); console.warn('Fastify privySecrets decorated:', !!decorated);
console.warn('Fastify config available:', !!fastify?.config); console.warn('Fastify config available:', !!fastify?.config);
// In production, also log Docker secret file existence
if (isProd) {
console.warn('Docker secrets check:');
console.warn(' /run/secrets/PRIVY_APP_ID exists:', fs.existsSync('/run/secrets/PRIVY_APP_ID'));
console.warn(' /run/secrets/PRIVY_APP_SECRET exists:', fs.existsSync('/run/secrets/PRIVY_APP_SECRET'));
console.warn(' /run/secrets/PRIVY_AUTHORIZATION_KEY exists:', fs.existsSync('/run/secrets/PRIVY_AUTHORIZATION_KEY'));
}
throw new Error('Missing required Privy environment variables'); throw new Error('Missing required Privy environment variables');
} }
console.log('appId', appId);
console.log('Privy client initialized successfully'); console.log('Privy client initialized successfully');
return new PrivyClient( return new PrivyClient(
@@ -137,16 +95,24 @@ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
* Authentication function for Privy API calls * Authentication function for Privy API calls
* @param url The URL for the API endpoint * @param url The URL for the API endpoint
* @param body The request body * @param body The request body
* @param fastify Optional Fastify instance to get secrets from decorator
* @returns The authorization signature * @returns The authorization signature
*/ */
export async function getAuthorizationSignature({url, body}: {url: string; body: object}) { export async function getAuthorizationSignature(
{url, body}: {url: string; body: object},
fastify?: FastifyInstance
) {
// Get app ID from Fastify decorator or env
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 || ''
const payload = { const payload = {
version: 1, version: 1,
method: 'POST', method: 'POST',
url, url,
body, body,
headers: { headers: {
'privy-app-id': process.env.PRIVY_APP_ID || '' 'privy-app-id': appId
} }
}; };
@@ -155,18 +121,16 @@ export async function getAuthorizationSignature({url, body}: {url: string; body:
const serializedPayload = canonicalize(payload) as string; const serializedPayload = canonicalize(payload) as string;
const serializedPayloadBuffer = Buffer.from(serializedPayload); const serializedPayloadBuffer = Buffer.from(serializedPayload);
// Resolve authorization key: Docker secrets in production, file/env in non-production // Resolve authorization key: from Fastify decorator (Infisical in prod), file/env in non-production
let resolvedKey: string | undefined let resolvedKey: string | undefined
if (process.env.NODE_ENV === 'production') {
// In production, read from Docker secrets mounted at /run/secrets/ // Try Fastify decorator first (loaded from Infisical in production)
const secretPath = '/run/secrets/PRIVY_AUTHORIZATION_KEY' if (decorated?.authKey) {
if (fs.existsSync(secretPath)) { resolvedKey = decorated.authKey
resolvedKey = fs.readFileSync(secretPath, 'utf8').trim() } else if (fastify?.config?.PRIVY_AUTHORIZATION_KEY) {
resolvedKey = fastify.config.PRIVY_AUTHORIZATION_KEY
} else { } else {
resolvedKey = process.env.PRIVY_AUTHORIZATION_KEY // Fallback to file or env var
}
} else {
// Non-production: allow file fallback
const filePath = process.env.PRIVY_AUTHORIZATION_KEY_FILE const filePath = process.env.PRIVY_AUTHORIZATION_KEY_FILE
const fromFile = filePath && fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8').trim() : undefined const fromFile = filePath && fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8').trim() : undefined
resolvedKey = fromFile || process.env.PRIVY_AUTHORIZATION_KEY || undefined resolvedKey = fromFile || process.env.PRIVY_AUTHORIZATION_KEY || undefined
@@ -200,24 +164,16 @@ export const makePrivyRequest = async <T>(
url: string, url: string,
body: object = {}, body: object = {},
requiresAuth = true, requiresAuth = true,
method: 'GET' | 'POST' = 'POST' method: 'GET' | 'POST' = 'POST',
fastify?: FastifyInstance
): Promise<T> => { ): Promise<T> => {
try { try {
// Resolve secrets: Docker secrets in production, env in non-production // Resolve secrets: from Fastify decorator (Infisical in prod), env in non-production
let appId: string const decorated = (fastify as any)?.privySecrets as { appId: string; appSecret: string } | undefined
let appSecret: string
if (process.env.NODE_ENV === 'production') { // Try Fastify decorator first (loaded from Infisical in production)
// In production, read from Docker secrets mounted at /run/secrets/ const appId = decorated?.appId || fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID || ''
const appIdPath = '/run/secrets/PRIVY_APP_ID' const appSecret = decorated?.appSecret || fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET || ''
const appSecretPath = '/run/secrets/PRIVY_APP_SECRET'
appId = (fs.existsSync(appIdPath) ? fs.readFileSync(appIdPath, 'utf8').trim() : undefined) || process.env.PRIVY_APP_ID || ''
appSecret = (fs.existsSync(appSecretPath) ? fs.readFileSync(appSecretPath, 'utf8').trim() : undefined) || process.env.PRIVY_APP_SECRET || ''
} else {
// Non-production: use env vars
appId = process.env.PRIVY_APP_ID || ''
appSecret = process.env.PRIVY_APP_SECRET || ''
}
let headers: Record<string, string> = { let headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -1023,11 +979,12 @@ export const sendTokenImpl = async (
export const getWalletBalanceImpl = async ( export const getWalletBalanceImpl = async (
address: string, address: string,
assets: Ticker[], assets: Ticker[],
chains: string[] chains: string[],
fastify?: FastifyInstance
): Promise<Balance[]> => { ): Promise<Balance[]> => {
try { try {
// First, get the user by wallet address using Privy Client // First, get the user by wallet address using Privy Client
const privy = getPrivyClient(); const privy = getPrivyClient(fastify);
// Get user by wallet address // Get user by wallet address
const user = await privy.getUserByWalletAddress(address); const user = await privy.getUserByWalletAddress(address);
@@ -1085,7 +1042,7 @@ export const getWalletBalanceImpl = async (
usd: string; usd: string;
}; };
}>; }>;
}>(balanceUrl, {}, true, 'GET'); }>(balanceUrl, {}, true, 'GET', fastify);
// Convert to Balance interface format (matching ManagingApiTypes.ts) // Convert to Balance interface format (matching ManagingApiTypes.ts)
const balances: Balance[] = balanceResponse.balances const balances: Balance[] = balanceResponse.balances
@@ -1211,8 +1168,8 @@ export async function getWalletBalance(
throw new Error('At least one chain is required for balance retrieval'); throw new Error('At least one chain is required for balance retrieval');
} }
// Call the getWalletBalanceImpl function // Call the getWalletBalanceImpl function, passing the Fastify instance from the request
const balances = await getWalletBalanceImpl(address, assets, chains); const balances = await getWalletBalanceImpl(address, assets, chains, this.server);
return { return {
success: true, success: true,

View File

@@ -86,7 +86,7 @@ export const autoConfig = {
* @see {@link https://github.com/fastify/fastify-env} * @see {@link https://github.com/fastify/fastify-env}
*/ */
export default fp(async (fastify) => { export default fp(async (fastify) => {
// In production, Privy secrets come from Docker secrets (mounted files), not env vars // In production, Privy secrets come from Infisical, not env vars
// Make them optional in the schema to avoid validation errors // Make them optional in the schema to avoid validation errors
const isProd = process.env.NODE_ENV === 'production' const isProd = process.env.NODE_ENV === 'production'
const required = isProd const required = isProd