Add bitwarden secret
This commit is contained in:
@@ -141,3 +141,4 @@ namespace Managing.Api.HealthChecks
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"author": "Oda",
|
"author": "Oda",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bitwarden/sdk-napi": "1.0.0",
|
||||||
"@fastify/autoload": "^6.0.0",
|
"@fastify/autoload": "^6.0.0",
|
||||||
"@fastify/cookie": "^11.0.1",
|
"@fastify/cookie": "^11.0.1",
|
||||||
"@fastify/cors": "^11.0.0",
|
"@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 {PrivyClient} from '@privy-io/server-auth'
|
||||||
import {ethers} from 'ethers'
|
import {ethers} from 'ethers'
|
||||||
import dotenv from 'dotenv'
|
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 Token from '../../generated/gmxsdk/abis/Token.json' with {type: 'json'}
|
||||||
import {ARBITRUM} from '../../generated/gmxsdk/configs/chains.js'
|
import {ARBITRUM} from '../../generated/gmxsdk/configs/chains.js'
|
||||||
import {TOKENS} from '../../generated/gmxsdk/configs/tokens.js'
|
import {TOKENS} from '../../generated/gmxsdk/configs/tokens.js'
|
||||||
@@ -14,8 +16,8 @@ import {getClientForAddress, getTokenDataFromTicker} from './gmx.js'
|
|||||||
import {Address} from 'viem'
|
import {Address} from 'viem'
|
||||||
import {Balance, Chain, Ticker} from '../../generated/ManagingApiTypes.js'
|
import {Balance, Chain, Ticker} from '../../generated/ManagingApiTypes.js'
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables in non-production only
|
||||||
dotenv.config()
|
if (process.env.NODE_ENV !== 'production') dotenv.config()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Privy Plugin
|
* Privy Plugin
|
||||||
@@ -54,9 +56,11 @@ declare module 'fastify' {
|
|||||||
* @returns The configured PrivyClient instance
|
* @returns The configured PrivyClient instance
|
||||||
*/
|
*/
|
||||||
export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
|
export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
|
||||||
const appId = fastify?.config?.PRIVY_APP_ID || process.env.PRIVY_APP_ID;
|
// Prefer Fastify decorator (populated by privy-secrets plugin)
|
||||||
const appSecret = fastify?.config?.PRIVY_APP_SECRET || process.env.PRIVY_APP_SECRET;
|
const decorated = (fastify as any)?.privySecrets as { appId: string; appSecret: string; authKey: string } | undefined
|
||||||
const authKey = fastify?.config?.PRIVY_AUTHORIZATION_KEY || process.env.PRIVY_AUTHORIZATION_KEY;
|
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) {
|
if (!appId || !appSecret || !authKey) {
|
||||||
console.warn('Missing Privy environment variables:');
|
console.warn('Missing Privy environment variables:');
|
||||||
@@ -66,6 +70,8 @@ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
|
|||||||
throw new Error('Missing required Privy environment variables');
|
throw new Error('Missing required Privy environment variables');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('appId', appId);
|
||||||
|
|
||||||
return new PrivyClient(
|
return new PrivyClient(
|
||||||
appId,
|
appId,
|
||||||
appSecret,
|
appSecret,
|
||||||
@@ -83,14 +89,14 @@ export const getPrivyClient = (fastify?: FastifyInstance): PrivyClient => {
|
|||||||
* @param body The request body
|
* @param body The request body
|
||||||
* @returns The authorization signature
|
* @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 = {
|
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': 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 serializedPayload = canonicalize(payload) as string;
|
||||||
const serializedPayloadBuffer = Buffer.from(serializedPayload);
|
const serializedPayloadBuffer = Buffer.from(serializedPayload);
|
||||||
|
|
||||||
// Get the authorization key from environment variables
|
// Resolve authorization key: use constant in production, file/env in non-production
|
||||||
const privateKeyAsString = (process.env.PRIVY_AUTHORIZATION_KEY ?? "").replace('wallet-auth:', '');
|
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
|
// Convert private key to PEM format and create a key object
|
||||||
const privateKeyAsPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyAsString}\n-----END PRIVATE KEY-----`;
|
const privateKeyAsPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyAsString}\n-----END PRIVATE KEY-----`;
|
||||||
@@ -130,15 +146,21 @@ export const makePrivyRequest = async <T>(
|
|||||||
method: 'GET' | 'POST' = 'POST'
|
method: 'GET' | 'POST' = 'POST'
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
try {
|
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> = {
|
let headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'privy-app-id': process.env.PRIVY_APP_ID ?? "",
|
'privy-app-id': appId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (requiresAuth) {
|
if (requiresAuth) {
|
||||||
// Create Basic Authentication header
|
// Create Basic Authentication header
|
||||||
const appId = process.env.PRIVY_APP_ID ?? "";
|
|
||||||
const appSecret = process.env.PRIVY_APP_SECRET ?? "";
|
|
||||||
const basicAuthString = `${appId}:${appSecret}`;
|
const basicAuthString = `${appId}:${appSecret}`;
|
||||||
const base64Auth = Buffer.from(basicAuthString).toString('base64');
|
const base64Auth = Buffer.from(basicAuthString).toString('base64');
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Fastify from 'fastify'
|
import Fastify from 'fastify'
|
||||||
|
import './bootstrap/privy-secrets.js'
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin'
|
||||||
import {createClient, RedisClientType} from 'redis'
|
import {createClient, RedisClientType} from 'redis'
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ import closeWithGrace from 'close-with-grace'
|
|||||||
|
|
||||||
// Import your application as a normal plugin.
|
// Import your application as a normal plugin.
|
||||||
import serviceApp from './app.js'
|
import serviceApp from './app.js'
|
||||||
|
// Import Privy secrets initialization
|
||||||
|
import {initializePrivySecrets} from './utils/privy-secrets.js'
|
||||||
|
|
||||||
// Idempotency storage using Redis
|
// Idempotency storage using Redis
|
||||||
interface IdempotencyEntry {
|
interface IdempotencyEntry {
|
||||||
@@ -166,6 +169,9 @@ const app = Fastify({
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
|
// Initialize Privy secrets from Bitwarden (production) or env (non-production)
|
||||||
|
await initializePrivySecrets()
|
||||||
|
|
||||||
// Initialize Redis connection
|
// Initialize Redis connection
|
||||||
await initializeRedis()
|
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