Add bitwarden secret

This commit is contained in:
2025-10-31 12:42:47 +07:00
parent 6fea759462
commit 759d7be5df
7 changed files with 217 additions and 12 deletions

View File

@@ -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",

View File

@@ -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' })

View File

@@ -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 <T>(
method: 'GET' | 'POST' = 'POST'
): Promise<T> => {
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<string, string> = {
'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');

View File

@@ -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()

View File

@@ -0,0 +1,89 @@
import {BitwardenClient, ClientSettings, DeviceType} from '@bitwarden/sdk-napi'
type SecretFetcher = (nameOrId: string) => Promise<string | undefined>
let clientPromise: Promise<BitwardenClient> | null = null
const secretCache = new Map<string, string>()
function getClient(): Promise<BitwardenClient> {
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()
}

View File

@@ -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<void> {
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 }