diff --git a/apps/web/instrumentation.ts b/apps/web/instrumentation.ts index ed4ae703f1..f2d8dbbb46 100644 --- a/apps/web/instrumentation.ts +++ b/apps/web/instrumentation.ts @@ -1,10 +1,15 @@ import * as Sentry from "@sentry/nextjs"; import { IS_PRODUCTION, PROMETHEUS_ENABLED, SENTRY_DSN } from "@/lib/constants"; +import { setupGlobalAgentProxy } from "@/lib/setupGlobalAgentProxy"; export const onRequestError = Sentry.captureRequestError; export const register = async () => { if (process.env.NEXT_RUNTIME === "nodejs") { + // Initialize global-agent proxy support (opt-in via USE_GLOBAL_AGENT_PROXY=1) + // Must run before any outbound HTTP requests to ensure proxy settings are applied + setupGlobalAgentProxy(); + if (PROMETHEUS_ENABLED) { await import("./instrumentation-node"); } diff --git a/apps/web/lib/env.ts b/apps/web/lib/env.ts index 03226736ac..f88bd0ba01 100644 --- a/apps/web/lib/env.ts +++ b/apps/web/lib/env.ts @@ -32,6 +32,9 @@ export const env = createEnv({ GOOGLE_SHEETS_REDIRECT_URL: z.string().optional(), HTTP_PROXY: z.string().url().optional(), HTTPS_PROXY: z.string().url().optional(), + GLOBAL_AGENT_NO_PROXY: z.string().optional(), + NO_PROXY: z.string().optional(), + USE_GLOBAL_AGENT_PROXY: z.enum(["1", "0"]).optional(), IMPRINT_URL: z .string() .url() @@ -159,6 +162,9 @@ export const env = createEnv({ GOOGLE_SHEETS_REDIRECT_URL: process.env.GOOGLE_SHEETS_REDIRECT_URL, HTTP_PROXY: process.env.HTTP_PROXY, HTTPS_PROXY: process.env.HTTPS_PROXY, + GLOBAL_AGENT_NO_PROXY: process.env.GLOBAL_AGENT_NO_PROXY, + NO_PROXY: process.env.NO_PROXY, + USE_GLOBAL_AGENT_PROXY: process.env.USE_GLOBAL_AGENT_PROXY, IMPRINT_URL: process.env.IMPRINT_URL, IMPRINT_ADDRESS: process.env.IMPRINT_ADDRESS, INVITE_DISABLED: process.env.INVITE_DISABLED, diff --git a/apps/web/lib/setupGlobalAgentProxy.ts b/apps/web/lib/setupGlobalAgentProxy.ts new file mode 100644 index 0000000000..979ea6010a --- /dev/null +++ b/apps/web/lib/setupGlobalAgentProxy.ts @@ -0,0 +1,42 @@ +import "server-only"; +import { logger } from "@formbricks/logger"; +import { env } from "./env"; + +declare global { + // eslint-disable-next-line no-var + var __FORMBRICKS_GLOBAL_AGENT_INITIALIZED: boolean | undefined; +} + +export const setupGlobalAgentProxy = (): void => { + if (globalThis.window !== undefined) { + return; + } + + if (globalThis.__FORMBRICKS_GLOBAL_AGENT_INITIALIZED) { + return; + } + + const isEnabled = env.USE_GLOBAL_AGENT_PROXY === "1"; + + if (!isEnabled) { + return; + } + + // Resolve NO_PROXY value from validated env + const noProxy = env.GLOBAL_AGENT_NO_PROXY ?? env.NO_PROXY; + + // Set GLOBAL_AGENT_NO_PROXY in process.env for global-agent to read + if (noProxy) { + // eslint-disable-next-line turbo/no-undeclared-env-vars + process.env.GLOBAL_AGENT_NO_PROXY = noProxy; + } + + try { + const { bootstrap } = require("global-agent"); + bootstrap(); + globalThis.__FORMBRICKS_GLOBAL_AGENT_INITIALIZED = true; + logger.info("Enabled global-agent proxy support for outbound HTTP requests"); + } catch (error) { + logger.error("Failed to enable global-agent proxy support", error); + } +}; diff --git a/apps/web/package.json b/apps/web/package.json index eb31d6dc5c..57b7b8f27e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -90,6 +90,7 @@ "file-loader": "6.2.0", "framer-motion": "12.10.0", "googleapis": "148.0.0", + "global-agent": "3.0.0", "heic-convert": "2.1.0", "https-proxy-agent": "7.0.6", "i18next": "25.5.2", diff --git a/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx b/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx index 068e07e051..ad6335aaef 100644 --- a/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx +++ b/docs/self-hosting/configuration/auth-sso/open-id-connect.mdx @@ -41,3 +41,12 @@ OIDC_SIGNING_ALGORITHM=HS256 - Restart your Formbricks instance. - You're all set! Users can now sign up & log in using their OIDC credentials. + +## Use behind a proxy + +If outbound traffic must pass through a corporate proxy and your IdP should bypass it: + +- Set `USE_GLOBAL_AGENT_PROXY=1` to enable proxy handling for all Auth.js HTTP requests. +- Set `HTTP_PROXY` / `HTTPS_PROXY` to your proxy endpoints. +- Set `NO_PROXY` (or `GLOBAL_AGENT_NO_PROXY`) to include your IdP host (for example, `auth.service.company.local,keycloak`). +- Restart the app to apply the settings. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 072e458600..ac24330eb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -321,6 +321,9 @@ importers: framer-motion: specifier: 12.10.0 version: 12.10.0(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + global-agent: + specifier: 3.0.0 + version: 3.0.0 googleapis: specifier: 148.0.0 version: 148.0.0(encoding@0.1.13) @@ -5391,6 +5394,10 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + boring-avatars@2.0.1: resolution: {integrity: sha512-TeBnZrp7WxHcQPuLhGQamklgNqaL7eUAUh3E11kFj9rTn0Hari2ZKVTchqNrp62UOHN/XOe5bZGcbzVGwHjHwg==} peerDependencies: @@ -5909,6 +5916,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -6090,6 +6100,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -6672,6 +6685,10 @@ packages: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -7244,6 +7261,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -7473,6 +7493,10 @@ packages: engines: {node: '>= 16'} hasBin: true + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -8755,6 +8779,10 @@ packages: ripemd160@2.0.2: resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + rollup@4.52.5: resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -8833,6 +8861,9 @@ packages: selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -8854,6 +8885,10 @@ packages: seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -9498,6 +9533,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -15930,6 +15969,8 @@ snapshots: boolbase@1.0.0: {} + boolean@3.2.0: {} + boring-avatars@2.0.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2): dependencies: react: 19.1.2 @@ -16435,6 +16476,8 @@ snapshots: detect-node-es@1.1.0: {} + detect-node@2.1.0: {} + didyoumean@1.2.2: {} diff@3.5.0: {} @@ -16677,6 +16720,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es6-error@4.1.1: {} + esbuild-register@3.6.0(esbuild@0.25.10): dependencies: debug: 4.4.3 @@ -17450,6 +17495,15 @@ snapshots: minipass: 4.2.8 path-scurry: 1.11.1 + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.3 + serialize-error: 7.0.1 + globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -18052,6 +18106,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: {} + json5@1.0.2: dependencies: minimist: 1.2.8 @@ -18320,6 +18376,10 @@ snapshots: marked@7.0.4: {} + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + math-intrinsics@1.1.0: {} md-to-react-email@5.0.5(react@19.1.2): @@ -19594,6 +19654,15 @@ snapshots: hash-base: 3.1.2 inherits: 2.0.4 + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + rollup@4.52.5: dependencies: '@types/estree': 1.0.8 @@ -19708,6 +19777,8 @@ snapshots: dependencies: parseley: 0.12.1 + semver-compare@1.0.0: {} + semver@5.7.2: {} semver@6.3.1: {} @@ -19720,6 +19791,10 @@ snapshots: seq-queue@0.0.5: {} + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -20468,6 +20543,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.13.1: {} + type-fest@0.20.2: {} type-fest@0.6.0: {}