Add bitwarden secret
This commit is contained in:
@@ -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",
|
||||
|
||||
43
src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts
Normal file
43
src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts
Normal 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' })
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
89
src/Managing.Web3Proxy/src/utils/bitwarden.ts
Normal file
89
src/Managing.Web3Proxy/src/utils/bitwarden.ts
Normal 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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
43
src/Managing.Web3Proxy/src/utils/privy-secrets.ts
Normal file
43
src/Managing.Web3Proxy/src/utils/privy-secrets.ts
Normal 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 }
|
||||
|
||||
Reference in New Issue
Block a user