diff --git a/src/server/index.ts b/src/server/index.ts index 9ff0ad1a6..5da36ba02 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -55,8 +55,9 @@ export const initServer = async () => { connectionTimeout: SERVER_CONNECTION_TIMEOUT, disableRequestLogging: true, // env.TRUST_PROXY is used to determine if the X-Forwarded-For header should be trusted. + // This option is force enabled for cloud-hosted Engines. // See: https://fastify.dev/docs/latest/Reference/Server/#trustproxy - trustProxy: env.TRUST_PROXY, + trustProxy: env.TRUST_PROXY || !!env.ENGINE_TIER, ...(env.ENABLE_HTTPS ? httpsObject : {}), }).withTypeProvider(); diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts index da0a45dae..8947a6d4b 100644 --- a/src/server/middleware/auth.ts +++ b/src/server/middleware/auth.ts @@ -1,14 +1,14 @@ import { parseJWT } from "@thirdweb-dev/auth"; import { ThirdwebAuth, - ThirdwebAuthUser, getToken as getJWT, + type ThirdwebAuthUser, } from "@thirdweb-dev/auth/fastify"; import { AsyncWallet } from "@thirdweb-dev/wallets/evm/wallets/async"; import { createHash } from "crypto"; -import { FastifyInstance } from "fastify"; -import { FastifyRequest } from "fastify/types/request"; -import jsonwebtoken, { JwtPayload } from "jsonwebtoken"; +import type { FastifyInstance } from "fastify"; +import type { FastifyRequest } from "fastify/types/request"; +import jsonwebtoken, { type JwtPayload } from "jsonwebtoken"; import { validate as uuidValidate } from "uuid"; import { getPermissions } from "../../db/permissions/getPermissions"; import { createToken } from "../../db/tokens/createToken"; @@ -87,7 +87,7 @@ export const withAuth = async (server: FastifyInstance) => { await revokeToken({ id: payload.jti }); } catch { logger({ - service: "worker", + service: "server", level: "error", message: `[Auth] Failed to revoke token ${payload.jti}`, }); @@ -267,10 +267,15 @@ const handleWebsocketAuth = async ( const isIpInAllowlist = await checkIpInAllowlist(req); if (!isIpInAllowlist) { + logger({ + service: "server", + level: "error", + message: `Unauthorized IP address: ${req.ip}`, + }); return { isAuthed: false, error: - "Unauthorized IP Address. See: https://portal.thirdweb.com/engine/features/security", + "Unauthorized IP address. See: https://portal.thirdweb.com/engine/features/security", }; } @@ -336,9 +341,14 @@ const handleKeypairAuth = async (args: { const isIpInAllowlist = await checkIpInAllowlist(req); if (!isIpInAllowlist) { - error = - "Unauthorized IP Address. See: https://portal.thirdweb.com/engine/features/security"; - throw error; + logger({ + service: "server", + level: "error", + message: `Unauthorized IP address: ${req.ip}`, + }); + throw new Error( + "Unauthorized IP address. See: https://portal.thirdweb.com/engine/features/security", + ); } return { isAuthed: true }; } catch (e) { @@ -391,12 +401,16 @@ const handleAccessToken = async ( } const isIpInAllowlist = await checkIpInAllowlist(req); - if (!isIpInAllowlist) { + logger({ + service: "server", + level: "error", + message: `Unauthorized IP address: ${req.ip}`, + }); return { isAuthed: false, error: - "Unauthorized IP Address. See: https://portal.thirdweb.com/engine/features/security", + "Unauthorized IP address. See: https://portal.thirdweb.com/engine/features/security", }; } @@ -505,7 +519,6 @@ const hashRequestBody = (req: FastifyRequest): string => { /** * Check if the request IP is in the allowlist. * Fetches cached config if available. - * env.TRUST_PROXY is used to determine if the X-Forwarded-For header should be trusted. * @param req FastifyRequest * @returns boolean * @async diff --git a/src/server/routes/system/health.ts b/src/server/routes/system/health.ts index a8fb11ee6..8eb857437 100644 --- a/src/server/routes/system/health.ts +++ b/src/server/routes/system/health.ts @@ -1,9 +1,9 @@ -import { Static, Type } from "@sinclair/typebox"; -import { FastifyInstance } from "fastify"; +import { Type, type Static } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; import { isDatabaseReachable } from "../../../db/client"; import { env } from "../../../utils/env"; -import { isRedisReachable, redis } from "../../../utils/redis/redis"; +import { isRedisReachable } from "../../../utils/redis/redis"; import { createCustomError } from "../../middleware/error"; type EngineFeature = "KEYPAIR_AUTH" | "CONTRACT_SUBSCRIPTIONS" | "IP_ALLOWLIST"; @@ -63,8 +63,8 @@ export async function healthCheck(fastify: FastifyInstance) { res.status(StatusCodes.OK).send({ status: "OK", - engineVersion: process.env.ENGINE_VERSION, - engineTier: process.env.ENGINE_TIER ?? "SELF_HOSTED", + engineVersion: env.ENGINE_VERSION, + engineTier: env.ENGINE_TIER ?? "SELF_HOSTED", features: getFeatures(), }); }, @@ -73,11 +73,9 @@ export async function healthCheck(fastify: FastifyInstance) { const getFeatures = (): EngineFeature[] => { // IP Allowlist is always available as a feature, but added as a feature for backwards compatibility. - const features: EngineFeature[] = ["IP_ALLOWLIST"]; + const features: EngineFeature[] = ["CONTRACT_SUBSCRIPTIONS", "IP_ALLOWLIST"]; if (env.ENABLE_KEYPAIR_AUTH) features.push("KEYPAIR_AUTH"); - // Contract Subscriptions requires Redis. - if (redis) features.push("CONTRACT_SUBSCRIPTIONS"); return features; }; diff --git a/src/utils/env.ts b/src/utils/env.ts index 79342a51a..7b51c7047 100644 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -25,13 +25,6 @@ export const UrlSchema = z { message: "Invalid URL" }, ); -const boolSchema = (defaultBool: "true" | "false") => - z - .string() - .default(defaultBool) - .refine((s) => s === "true" || s === "false", "must be 'true' or 'false'") - .transform((s) => s === "true"); - export const env = createEnv({ server: { NODE_ENV: z @@ -48,6 +41,8 @@ export const env = createEnv({ .array(z.enum(["server", "worker", "cache", "websocket"])) .parse(s.split(",")), ), + ENGINE_VERSION: z.string().optional(), + ENGINE_TIER: z.string().optional(), THIRDWEB_API_SECRET_KEY: z.string().min(1), ADMIN_WALLET_ADDRESS: z.string().min(1), ENCRYPTION_PASSWORD: z.string().min(1), @@ -58,15 +53,15 @@ export const env = createEnv({ ), PORT: z.coerce.number().default(3005), HOST: z.string().default("0.0.0.0"), - ENABLE_HTTPS: boolSchema("false"), + ENABLE_HTTPS: z.coerce.boolean().default(false), HTTPS_PASSPHRASE: z.string().default("thirdweb-engine"), - TRUST_PROXY: z.boolean().default(false), + TRUST_PROXY: z.coerce.boolean().default(false), CLIENT_ANALYTICS_URL: z .union([UrlSchema, z.literal("")]) .default("https://c.thirdweb.com/event"), SDK_BATCH_TIME_LIMIT: z.coerce.number().default(0), SDK_BATCH_SIZE_LIMIT: z.coerce.number().default(100), - ENABLE_KEYPAIR_AUTH: boolSchema("false"), + ENABLE_KEYPAIR_AUTH: z.coerce.boolean().default(false), CONTRACT_SUBSCRIPTIONS_DELAY_SECONDS: z.coerce .number() .nonnegative() @@ -82,7 +77,7 @@ export const env = createEnv({ // Prometheus METRICS_PORT: z.coerce.number().default(4001), - METRICS_ENABLED: boolSchema("true"), + METRICS_ENABLED: z.coerce.boolean().default(true), /** * Limits @@ -108,6 +103,8 @@ export const env = createEnv({ NODE_ENV: process.env.NODE_ENV, LOG_LEVEL: process.env.LOG_LEVEL, LOG_SERVICES: process.env.LOG_SERVICES, + ENGINE_VERSION: process.env.ENGINE_VERSION, + ENGINE_TIER: process.env.ENGINE_TIER, THIRDWEB_API_SECRET_KEY: process.env.THIRDWEB_API_SECRET_KEY, ADMIN_WALLET_ADDRESS: process.env.ADMIN_WALLET_ADDRESS, ENCRYPTION_PASSWORD: process.env.ENCRYPTION_PASSWORD,