diff --git a/src/Managing.Api/HealthChecks/CandleDataDetailedHealthCheck.cs b/src/Managing.Api/HealthChecks/CandleDataDetailedHealthCheck.cs index 1c9e7b4b..56636cf7 100644 --- a/src/Managing.Api/HealthChecks/CandleDataDetailedHealthCheck.cs +++ b/src/Managing.Api/HealthChecks/CandleDataDetailedHealthCheck.cs @@ -141,3 +141,4 @@ namespace Managing.Api.HealthChecks } + diff --git a/src/Managing.Web3Proxy/package.json b/src/Managing.Web3Proxy/package.json index e6466446..2b834f0b 100644 --- a/src/Managing.Web3Proxy/package.json +++ b/src/Managing.Web3Proxy/package.json @@ -29,6 +29,7 @@ "author": "Oda", "license": "MIT", "dependencies": { + "@bitwarden/sdk-napi": "1.0.0", "@fastify/autoload": "^6.0.0", "@fastify/cookie": "^11.0.1", "@fastify/cors": "^11.0.0", diff --git a/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts b/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts new file mode 100644 index 00000000..2fc45c1f --- /dev/null +++ b/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts @@ -0,0 +1,43 @@ +import fp from 'fastify-plugin' +import {getSecret} from '../../utils/bitwarden.js' + +declare module 'fastify' { + interface FastifyInstance { + privySecrets: { + appId: string + appSecret: string + authKey: string + } + } +} + +export default fp(async function (fastify) { + const isProd = process.env.NODE_ENV === 'production' + + // Resolve secrets + const [authKeyBw, appIdBw, appSecretBw] = isProd + ? await Promise.all([ + getSecret('PRIVY_AUTHORIZATION_KEY'), + getSecret('PRIVY_APP_ID'), + getSecret('PRIVY_APP_SECRET') + ]) + : [undefined, undefined, undefined] + + const appId = appIdBw || process.env.PRIVY_APP_ID || '' + const appSecret = appSecretBw || process.env.PRIVY_APP_SECRET || '' + const authKey = authKeyBw || process.env.PRIVY_AUTHORIZATION_KEY || '' + + if (!appId || !appSecret || !authKey) { + fastify.log.warn('Privy secrets not fully resolved at plugin load.') + } + + fastify.decorate('privySecrets', { + appId, + appSecret, + authKey + }) + + fastify.log.info('Privy secrets decorated on Fastify instance') +}, { name: 'privy-secrets' }) + + diff --git a/src/Managing.Web3Proxy/src/plugins/custom/privy.ts b/src/Managing.Web3Proxy/src/plugins/custom/privy.ts index 4b748a60..f310c49e 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/privy.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/privy.ts @@ -6,6 +6,8 @@ import crypto from 'crypto' import {PrivyClient} from '@privy-io/server-auth' import {ethers} from 'ethers' import dotenv from 'dotenv' +import fs from 'fs' +import {PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_AUTHORIZATION_KEY} from '../../utils/privy-secrets.js' import Token from '../../generated/gmxsdk/abis/Token.json' with {type: 'json'} import {ARBITRUM} from '../../generated/gmxsdk/configs/chains.js' import {TOKENS} from '../../generated/gmxsdk/configs/tokens.js' @@ -14,8 +16,8 @@ import {getClientForAddress, getTokenDataFromTicker} from './gmx.js' import {Address} from 'viem' import {Balance, Chain, Ticker} from '../../generated/ManagingApiTypes.js' -// Load environment variables -dotenv.config() +// Load environment variables in non-production only +if (process.env.NODE_ENV !== 'production') dotenv.config() /** * Privy Plugin @@ -54,9 +56,11 @@ declare module 'fastify' { * @returns The configured PrivyClient instance */ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => { - const appId = fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID; - const appSecret = fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET; - const authKey = fastify?.config?.PRIVY_AUTHORIZATION_KEY || process.env.PRIVY_AUTHORIZATION_KEY; + // Prefer Fastify decorator (populated by privy-secrets plugin) + const decorated = (fastify as any)?.privySecrets as { appId: string; appSecret: string; authKey: string } | undefined + 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 + const authKey = decorated?.authKey || fastify?.config?.PRIVY_AUTHORIZATION_KEY || process.env.PRIVY_AUTHORIZATION_KEY if (!appId || !appSecret || !authKey) { console.warn('Missing Privy environment variables:'); @@ -66,6 +70,8 @@ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => { throw new Error('Missing required Privy environment variables'); } + console.log('appId', appId); + return new PrivyClient( appId, appSecret, @@ -83,14 +89,14 @@ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => { * @param body The request body * @returns The authorization signature */ -export function getAuthorizationSignature({url, body}: {url: string; body: object}) { +export async function getAuthorizationSignature({url, body}: {url: string; body: object}) { const payload = { version: 1, method: 'POST', url, body, headers: { - 'privy-app-id': process.env.PRIVY_APP_ID + 'privy-app-id': process.env.NODE_ENV === 'production' ? (PRIVY_APP_ID ?? process.env.PRIVY_APP_ID) : process.env.PRIVY_APP_ID } }; @@ -99,8 +105,18 @@ export function getAuthorizationSignature({url, body}: {url: string; body: objec const serializedPayload = canonicalize(payload) as string; const serializedPayloadBuffer = Buffer.from(serializedPayload); - // Get the authorization key from environment variables - const privateKeyAsString = (process.env.PRIVY_AUTHORIZATION_KEY ?? "").replace('wallet-auth:', ''); + // Resolve authorization key: use constant in production, file/env in non-production + let resolvedKey: string | undefined + if (process.env.NODE_ENV === 'production') { + resolvedKey = PRIVY_AUTHORIZATION_KEY + } else { + const filePath = process.env.PRIVY_AUTHORIZATION_KEY_FILE + const fromFile = filePath && fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8').trim() : undefined + resolvedKey = fromFile || process.env.PRIVY_AUTHORIZATION_KEY || undefined + } + + if (!resolvedKey) throw new Error('PRIVY_AUTHORIZATION_KEY not available') + const privateKeyAsString = resolvedKey.replace('wallet-auth:', ''); // Convert private key to PEM format and create a key object const privateKeyAsPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyAsString}\n-----END PRIVATE KEY-----`; @@ -130,15 +146,21 @@ export const makePrivyRequest = async ( method: 'GET' | 'POST' = 'POST' ): Promise => { try { + // Use constants in production, env in non-production + const appId = process.env.NODE_ENV === 'production' + ? (PRIVY_APP_ID ?? process.env.PRIVY_APP_ID ?? "") + : (process.env.PRIVY_APP_ID ?? "") + const appSecret = process.env.NODE_ENV === 'production' + ? (PRIVY_APP_SECRET ?? process.env.PRIVY_APP_SECRET ?? "") + : (process.env.PRIVY_APP_SECRET ?? "") + let headers: Record = { 'Content-Type': 'application/json', - 'privy-app-id': process.env.PRIVY_APP_ID ?? "", + 'privy-app-id': appId, }; if (requiresAuth) { // Create Basic Authentication header - const appId = process.env.PRIVY_APP_ID ?? ""; - const appSecret = process.env.PRIVY_APP_SECRET ?? ""; const basicAuthString = `${appId}:${appSecret}`; const base64Auth = Buffer.from(basicAuthString).toString('base64'); diff --git a/src/Managing.Web3Proxy/src/server.ts b/src/Managing.Web3Proxy/src/server.ts index db3c2474..cfa3da44 100644 --- a/src/Managing.Web3Proxy/src/server.ts +++ b/src/Managing.Web3Proxy/src/server.ts @@ -6,6 +6,7 @@ */ import Fastify from 'fastify' +import './bootstrap/privy-secrets.js' import fp from 'fastify-plugin' import {createClient, RedisClientType} from 'redis' @@ -14,6 +15,8 @@ import closeWithGrace from 'close-with-grace' // Import your application as a normal plugin. import serviceApp from './app.js' +// Import Privy secrets initialization +import {initializePrivySecrets} from './utils/privy-secrets.js' // Idempotency storage using Redis interface IdempotencyEntry { @@ -166,6 +169,9 @@ const app = Fastify({ }) async function init () { + // Initialize Privy secrets from Bitwarden (production) or env (non-production) + await initializePrivySecrets() + // Initialize Redis connection await initializeRedis() diff --git a/src/Managing.Web3Proxy/src/utils/bitwarden.ts b/src/Managing.Web3Proxy/src/utils/bitwarden.ts new file mode 100644 index 00000000..407bca17 --- /dev/null +++ b/src/Managing.Web3Proxy/src/utils/bitwarden.ts @@ -0,0 +1,89 @@ +import {BitwardenClient, ClientSettings, DeviceType} from '@bitwarden/sdk-napi' + +type SecretFetcher = (nameOrId: string) => Promise + +let clientPromise: Promise | null = null +const secretCache = new Map() + +function getClient(): Promise { + if (clientPromise) return clientPromise + + clientPromise = (async () => { + const accessToken = process.env.BITWARDEN_ACCESS_TOKEN + if (!accessToken) throw new Error('BITWARDEN_ACCESS_TOKEN is not set') + + const settings: ClientSettings = { + apiUrl: process.env.BITWARDEN_API_URL || 'https://vault.bitwarden.com/api', + identityUrl: process.env.BITWARDEN_IDENTITY_URL || 'https://vault.bitwarden.com/identity', + userAgent: 'Managing-Web3Proxy', + deviceType: DeviceType.SDK, + } + + const client = new BitwardenClient(settings) + const stateFile = process.env.BITWARDEN_STATE_FILE || '/tmp/bw-sdk-state.json' + await client.auth().loginAccessToken(accessToken, stateFile) + return client + })() + + return clientPromise +} + +export const getSecret: SecretFetcher = async (nameOrId: string) => { + if (secretCache.has(nameOrId)) return secretCache.get(nameOrId) + + const client = await getClient() + const organizationId = process.env.BITWARDEN_ORGANIZATION_ID + + // Try fetch by ID first + try { + const byId = await client.secrets().get(nameOrId) + if (byId?.value) { + secretCache.set(nameOrId, byId.value) + return byId.value + } + } catch {} + + // Optional: fetch by configured secret id env + const envKey = process.env[`BITWARDEN_SECRET_ID_${nameOrId}`] + if (envKey) { + try { + const res = await client.secrets().get(envKey) + if (res?.value) { + secretCache.set(nameOrId, res.value) + return res.value + } + } catch {} + } + + // If we have organization_id, try listing secrets and finding by name/key + if (organizationId) { + try { + const secretsList = await client.secrets().list(organizationId) + if (secretsList?.data) { + // Find secret by key (name) matching nameOrId + const found = secretsList.data.find((s: any) => s.key === nameOrId || s.id === nameOrId) + if (found) { + // Fetch the full secret by ID to get the value + try { + const secret = await client.secrets().get(found.id) + if (secret?.value) { + secretCache.set(nameOrId, secret.value) + return secret.value + } + } catch {} + } + } + } catch (error) { + console.error(`Failed to list secrets for organization ${organizationId}:`, error) + } + } + + return undefined +} + +export function clearSecretCache() { + secretCache.clear() +} + + + diff --git a/src/Managing.Web3Proxy/src/utils/privy-secrets.ts b/src/Managing.Web3Proxy/src/utils/privy-secrets.ts new file mode 100644 index 00000000..14a117a2 --- /dev/null +++ b/src/Managing.Web3Proxy/src/utils/privy-secrets.ts @@ -0,0 +1,43 @@ +import {getSecret} from './bitwarden.js' + +let PRIVY_APP_ID: string | undefined +let PRIVY_APP_SECRET: string | undefined +let PRIVY_AUTHORIZATION_KEY: string | undefined +let initialized = false + +// Initialize secrets from Bitwarden in production +export async function initializePrivySecrets(): Promise { + if (initialized) return + + if (process.env.NODE_ENV === 'production') { + try { + const [authKey, appId, appSecret] = await Promise.all([ + getSecret('PRIVY_AUTHORIZATION_KEY'), + getSecret('PRIVY_APP_ID'), + getSecret('PRIVY_APP_SECRET') + ]) + + console.log('appId', appId); + + PRIVY_AUTHORIZATION_KEY = authKey || process.env.PRIVY_AUTHORIZATION_KEY + PRIVY_APP_ID = appId || process.env.PRIVY_APP_ID + PRIVY_APP_SECRET = appSecret || process.env.PRIVY_APP_SECRET + } catch (error) { + console.error('Failed to load Privy secrets from Bitwarden:', error) + // Fallback to env vars + PRIVY_AUTHORIZATION_KEY = process.env.PRIVY_AUTHORIZATION_KEY + PRIVY_APP_ID = process.env.PRIVY_APP_ID + PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET + } + } else { + // In non-production, use env vars directly + PRIVY_AUTHORIZATION_KEY = process.env.PRIVY_AUTHORIZATION_KEY + PRIVY_APP_ID = process.env.PRIVY_APP_ID + PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET + } + + initialized = true +} + +export { PRIVY_APP_ID, PRIVY_APP_SECRET, PRIVY_AUTHORIZATION_KEY } +