diff --git a/definition-proxy-api b/definition-proxy-api index 9dd137a9..d2d7f9d7 100644 --- a/definition-proxy-api +++ b/definition-proxy-api @@ -1,4 +1,45 @@ { "schemaVersion": 2, - "dockerfilePath": "./src/Managing.Web3Proxy/Dockerfile-web3proxy" - } \ No newline at end of file + "dockerfilePath": "./src/Managing.Web3Proxy/Dockerfile-web3proxy", + "preDeployFunction": function(captainAppObj, dockerUpdateObject) { + // Define the array of all three secrets to be mounted in the container + // File.Name must match the filename that code reads from /run/secrets/ + dockerUpdateObject.TaskTemplate.ContainerSpec.Secrets = [ + { + // Secret 1: PRIVY_APP_ID + File: { + Name: "PRIVY_APP_ID", // Filename in the container at /run/secrets/PRIVY_APP_ID + UID: '0', + GID: '0', + Mode: 292 + }, + SecretID: "4kk2gw2945x8hdatyhuhomtk2", // <<-- REPLACE THIS with the actual Secret ID from 'docker secret ls' + SecretName: "privy_app_id" + }, + { + // Secret 2: PRIVY_APP_SECRET + File: { + Name: "PRIVY_APP_SECRET", // Filename in the container at /run/secrets/PRIVY_APP_SECRET + UID: '0', + GID: '0', + Mode: 292 + }, + SecretID: "y81woaa2388zidk35gul7y9n6", // <<-- REPLACE THIS with the actual Secret ID from 'docker secret ls' + SecretName: "privy_app_secret" + }, + { + // Secret 3: PRIVY_AUTHORIZATION_KEY + File: { + Name: "PRIVY_AUTHORIZATION_KEY", // Filename in the container at /run/secrets/PRIVY_AUTHORIZATION_KEY + UID: '0', + GID: '0', + Mode: 292 + }, + SecretID: "spnc1vle5q560jsoy72ng0vtc", // <<-- REPLACE THIS with the actual Secret ID from 'docker secret ls' + SecretName: "privy_auth_key" + } + ]; + + return Promise.resolve(); + } +} diff --git a/src/Managing.Web3Proxy/Dockerfile-web3proxy b/src/Managing.Web3Proxy/Dockerfile-web3proxy index 7fbf909e..a7a5613b 100644 --- a/src/Managing.Web3Proxy/Dockerfile-web3proxy +++ b/src/Managing.Web3Proxy/Dockerfile-web3proxy @@ -1,38 +1,22 @@ -# --- Stage 1: Build & Install Dependencies --- - FROM node:18-alpine AS builder +# Use an official Node.js image as the base +FROM node:22.14.0-alpine - WORKDIR /app - - # Copy package.json and lock file (if used) from source subdir - COPY /src/Managing.Web3Proxy/package*.json ./ - - ENV NODE_ENV production - - # Install dependencies (development dependencies are fine here) - RUN npm ci --no-audit --fund=false - - # Copy the rest of the source code - COPY src/Managing.Web3Proxy/ . - - # Run the build script - RUN npm run build - - # --- Stage 2: Production Runtime --- - FROM node:22.14.0-alpine AS production - - WORKDIR /app - - # Copy essential files from the builder stage - COPY --from=builder /app/package*.json ./ - COPY --from=builder /app/node_modules/ ./node_modules/ - # Copy the compiled application code (assuming 'dist' is the output folder) - COPY --from=builder /app/dist/ ./dist/ - - # Set production environment again for the runtime stage - ENV NODE_ENV production - - EXPOSE 4111 - - # Start the application from the compiled output - CMD ["fastify", "start", "-l", "info", "dist/app.js"] - \ No newline at end of file +# Set the working directory in the container +WORKDIR /app + +# COPY package*.json ./ +COPY /src/Managing.Web3Proxy/package.json ./ + +# Declaring env +ENV NODE_ENV production + +# Install dependencies with the --legacy-peer-deps flag to bypass peer dependency conflicts +RUN npm install + +COPY src/Managing.Web3Proxy/ . + +RUN npm run build + +EXPOSE 4111 + +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/src/Managing.Web3Proxy/package-lock.json b/src/Managing.Web3Proxy/package-lock.json index e5479798..cfeada92 100644 --- a/src/Managing.Web3Proxy/package-lock.json +++ b/src/Managing.Web3Proxy/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@bitwarden/sdk-napi": "1.0.0", "@fastify/autoload": "^6.0.0", "@fastify/cookie": "^11.0.1", "@fastify/cors": "^11.0.0", @@ -82,85 +81,6 @@ "node": ">=18" } }, - "node_modules/@bitwarden/sdk-napi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-napi/-/sdk-napi-1.0.0.tgz", - "integrity": "sha512-KrOXiuuQdVBvIM4l7XGrKl8otA9tSEKHjRQr0nF71eyCmIDzOV2kt/gchgLK42exGGuD/JcmmgnXx/j+U6W/+g==", - "license": "SEE LICENSE IN LICENSE", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@bitwarden/sdk-napi-darwin-arm64": "1.0.0", - "@bitwarden/sdk-napi-darwin-x64": "1.0.0", - "@bitwarden/sdk-napi-linux-x64-gnu": "1.0.0", - "@bitwarden/sdk-napi-win32-x64-msvc": "1.0.0" - } - }, - "node_modules/@bitwarden/sdk-napi-darwin-arm64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-napi-darwin-arm64/-/sdk-napi-darwin-arm64-1.0.0.tgz", - "integrity": "sha512-0W6P2VByGFqYIf6bkI7qKWlXPGhG9KFibrN14pvllFIkbNRaz3hktMOLJ/JdWZVmdHrz0R5xmAVldbIWNMjFOA==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@bitwarden/sdk-napi-darwin-x64": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-napi-darwin-x64/-/sdk-napi-darwin-x64-1.0.0.tgz", - "integrity": "sha512-Esyg8sktE50U+g12lL/xe68hgc5y78jPPV9maNcKTO9EWdde4RIx9u3xEpbYk5fyHaeMFXdo3TK8A8qLD34X5w==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@bitwarden/sdk-napi-linux-x64-gnu": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-napi-linux-x64-gnu/-/sdk-napi-linux-x64-gnu-1.0.0.tgz", - "integrity": "sha512-PBGEOq9saYuBBthCq5jAr7eQSyEmQylB+kwsuT0j666VXT124a3YjDGgobPYaR9KN/IRDQ5h2Tysuy1pojx10A==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@bitwarden/sdk-napi-win32-x64-msvc": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-napi-win32-x64-msvc/-/sdk-napi-win32-x64-msvc-1.0.0.tgz", - "integrity": "sha512-cu92XYjsOWEVECyohGtLxiXcMJMVui+nDJtyNg52iGDeAzWQ7QZh0/bcjkymTCuGoqRD3sT0cCs+5PbW66K2XQ==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", diff --git a/src/Managing.Web3Proxy/package.json b/src/Managing.Web3Proxy/package.json index 2b834f0b..e6466446 100644 --- a/src/Managing.Web3Proxy/package.json +++ b/src/Managing.Web3Proxy/package.json @@ -29,7 +29,6 @@ "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 index 2fc45c1f..4b231d07 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/privy-secrets.ts @@ -1,5 +1,5 @@ import fp from 'fastify-plugin' -import {getSecret} from '../../utils/bitwarden.js' +import fs from 'fs' declare module 'fastify' { interface FastifyInstance { @@ -11,24 +11,53 @@ declare module 'fastify' { } } +// Docker secrets are mounted at /run/secrets/ by default in CapRover +const readSecretFile = (secretName: string): string | undefined => { + const secretPath = `/run/secrets/${secretName}` + if (fs.existsSync(secretPath)) { + try { + return fs.readFileSync(secretPath, 'utf8').trim() + } catch (error) { + console.error(`Failed to read secret file ${secretPath}:`, error) + return undefined + } + } + return undefined +} + 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] + let appId: string + let appSecret: string + let authKey: string - 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 (isProd) { + // In production, read from Docker secrets (mounted files) + appId = readSecretFile('PRIVY_APP_ID') || process.env.PRIVY_APP_ID || '' + appSecret = readSecretFile('PRIVY_APP_SECRET') || process.env.PRIVY_APP_SECRET || '' + authKey = readSecretFile('PRIVY_AUTHORIZATION_KEY') || process.env.PRIVY_AUTHORIZATION_KEY || '' + } else { + // In non-production, use env vars or file paths + const readMaybeFile = (envKey: string, fileKey: string): string | undefined => { + const filePath = process.env[fileKey] + if (filePath && fs.existsSync(filePath)) return fs.readFileSync(filePath, 'utf8').trim() + return process.env[envKey] + } + + appId = readMaybeFile('PRIVY_APP_ID', 'PRIVY_APP_ID_FILE') || '' + appSecret = readMaybeFile('PRIVY_APP_SECRET', 'PRIVY_APP_SECRET_FILE') || '' + authKey = readMaybeFile('PRIVY_AUTHORIZATION_KEY', 'PRIVY_AUTHORIZATION_KEY_FILE') || '' + } if (!appId || !appSecret || !authKey) { fastify.log.warn('Privy secrets not fully resolved at plugin load.') + fastify.log.warn({ + appId: !!appId, + appSecret: !!appSecret, + authKey: !!authKey, + isProd + }, 'Privy secrets status') } fastify.decorate('privySecrets', { @@ -39,5 +68,3 @@ export default fp(async function (fastify) { 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 f310c49e..1c970c6f 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/privy.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/privy.ts @@ -7,7 +7,6 @@ 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' @@ -96,7 +95,7 @@ export async function getAuthorizationSignature({url, body}: {url: string; body: url, body, headers: { - 'privy-app-id': process.env.NODE_ENV === 'production' ? (PRIVY_APP_ID ?? process.env.PRIVY_APP_ID) : process.env.PRIVY_APP_ID + 'privy-app-id': process.env.PRIVY_APP_ID || '' } }; @@ -105,11 +104,18 @@ export async function getAuthorizationSignature({url, body}: {url: string; body: const serializedPayload = canonicalize(payload) as string; const serializedPayloadBuffer = Buffer.from(serializedPayload); - // Resolve authorization key: use constant in production, file/env in non-production + // Resolve authorization key: Docker secrets in production, file/env in non-production let resolvedKey: string | undefined if (process.env.NODE_ENV === 'production') { - resolvedKey = PRIVY_AUTHORIZATION_KEY + // In production, read from Docker secrets mounted at /run/secrets/ + const secretPath = '/run/secrets/PRIVY_AUTHORIZATION_KEY' + if (fs.existsSync(secretPath)) { + resolvedKey = fs.readFileSync(secretPath, 'utf8').trim() + } else { + resolvedKey = process.env.PRIVY_AUTHORIZATION_KEY + } } else { + // Non-production: allow file fallback 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 @@ -146,13 +152,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 ?? "") + // Resolve secrets: Docker secrets in production, env in non-production + let appId: string + let appSecret: string + + if (process.env.NODE_ENV === 'production') { + // In production, read from Docker secrets mounted at /run/secrets/ + const appIdPath = '/run/secrets/PRIVY_APP_ID' + 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 = { 'Content-Type': 'application/json', diff --git a/src/Managing.Web3Proxy/src/server.ts b/src/Managing.Web3Proxy/src/server.ts index cfa3da44..db3c2474 100644 --- a/src/Managing.Web3Proxy/src/server.ts +++ b/src/Managing.Web3Proxy/src/server.ts @@ -6,7 +6,6 @@ */ import Fastify from 'fastify' -import './bootstrap/privy-secrets.js' import fp from 'fastify-plugin' import {createClient, RedisClientType} from 'redis' @@ -15,8 +14,6 @@ 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 { @@ -169,9 +166,6 @@ 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 deleted file mode 100644 index 407bca17..00000000 --- a/src/Managing.Web3Proxy/src/utils/bitwarden.ts +++ /dev/null @@ -1,89 +0,0 @@ -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 deleted file mode 100644 index 14a117a2..00000000 --- a/src/Managing.Web3Proxy/src/utils/privy-secrets.ts +++ /dev/null @@ -1,43 +0,0 @@ -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 } -