Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions .github/actions/cache-build-web/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ inputs:
description: "Set E2E Testing Mode"
required: false
default: "0"
turbo_token:
description: "Turborepo token"
required: false
turbo_team:
description: "Turborepo team"
required: false

runs:
using: "composite"
Expand Down Expand Up @@ -38,15 +32,18 @@ runs:
run: echo "cache-hit=${{ steps.cache-build.outputs.cache-hit }}" >> "$GITHUB_OUTPUT"
shell: bash

# pnpm + Node are set up unconditionally: even on a build-cache hit the steps
# below still run `pnpm dev:setup` (regenerates .env), which needs the pnpm
# binary on PATH. The expensive dependency install and build stay gated on a
# cache miss.
- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Setup Node.js (version from .nvmrc)
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"
if: steps.cache-build.outputs.cache-hit != 'true'

- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
if: steps.cache-build.outputs.cache-hit != 'true'
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile --config.platform=linux --config.architecture=x64
Expand All @@ -68,6 +65,3 @@ runs:
pnpm build --filter=@formbricks/web...
if: steps.cache-build.outputs.cache-hit != 'true'
shell: bash
env:
TURBO_TOKEN: ${{ inputs.turbo_token }}
TURBO_TEAM: ${{ inputs.turbo_team }}
7 changes: 4 additions & 3 deletions .github/workflows/api-v3-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ jobs:
with:
persist-credentials: false

- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Setup Node.js (version from .nvmrc)
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"

- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
cache: pnpm

# Root importer only: installs the lockfile-pinned @redocly/cli (and other root devDeps)
# without building the whole workspace. bundle.mjs falls back to pinned npx when absent.
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/build-web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,3 @@ jobs:
id: cache-build-web
with:
e2e_testing_mode: "0"
turbo_token: ${{ secrets.TURBO_TOKEN }}
turbo_team: ${{ vars.TURBO_TEAM }}
4 changes: 0 additions & 4 deletions .github/workflows/docker-build-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ on:
permissions:
contents: read

env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}

jobs:
validate-docker-build:
name: Validate Docker Build
Expand Down
13 changes: 5 additions & 8 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ on:
merge_group:
workflow_dispatch:

env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}

# Cancel superseded runs on the same PR/ref (matches pr.yml).
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand Down Expand Up @@ -88,15 +84,16 @@ jobs:
fi
shell: bash

- name: Install pnpm
if: steps.harness.outputs.present == 'true'
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Setup Node.js (version from .nvmrc)
if: steps.harness.outputs.present == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"

- name: Install pnpm
if: steps.harness.outputs.present == 'true'
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
cache: pnpm

- name: Install dependencies
if: steps.harness.outputs.present == 'true'
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: ./.github/actions/dangerous-git-checkout

- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Setup Node.js (version from .nvmrc)
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"

- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile --config.platform=linux --config.architecture=x64
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/sonarqube.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,21 @@ jobs:
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis

- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Setup Node.js (version from .nvmrc)
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"
cache: pnpm

- name: Setup Java 21
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
with:
distribution: temurin
java-version: "21"

- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Install dependencies
run: pnpm install --frozen-lockfile --config.platform=linux --config.architecture=x64

Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/actions/dangerous-git-checkout

- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Setup Node.js (version from .nvmrc)
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"

- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile --config.platform=linux --config.architecture=x64
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/translation-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ jobs:
- 'packages/i18n-utils/**'
- '**/i18n.lock'

- name: Install pnpm
if: steps.changes.outputs.translations == 'true'
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0

- name: Setup Node.js (version from .nvmrc)
if: steps.changes.outputs.translations == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"

- name: Install pnpm
if: steps.changes.outputs.translations == 'true'
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
cache: pnpm

- name: Install dependencies
if: steps.changes.outputs.translations == 'true'
Expand Down
15 changes: 15 additions & 0 deletions apps/web/lib/image-hosts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,19 @@ describe("isExternalImageSrc", () => {
test("does not include the deployment domain in the optimizable host allowlist", () => {
expect(OPTIMIZABLE_IMAGE_HOSTS).not.toContain("app.formbricks.com");
});

test("treats an allowlisted public host over plain http as external (protocol mismatches remotePatterns' https)", () => {
expect(isExternalImageSrc("http://images.unsplash.com/photo-1.jpg")).toBe(true);
expect(isExternalImageSrc("http://avatars.githubusercontent.com/u/1")).toBe(true);
});

test("treats an allowlisted loopback host over https as external (protocol mismatches remotePatterns' http)", () => {
expect(isExternalImageSrc("https://localhost:3000/x.png")).toBe(true);
expect(isExternalImageSrc("https://127.0.0.1:3000/x.png")).toBe(true);
});

test("treats an allowlisted loopback host over http as optimizable", () => {
expect(isExternalImageSrc("http://localhost:3000/x.png")).toBe(false);
expect(isExternalImageSrc("http://127.0.0.1:3000/x.png")).toBe(false);
});
});
24 changes: 19 additions & 5 deletions apps/web/lib/image-hosts.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { type StaticImageData } from "next/image";
import { OPTIMIZABLE_IMAGE_HOSTS } from "./optimizable-image-hosts.mjs";
import { LOOPBACK_HOSTS, OPTIMIZABLE_IMAGE_HOSTS } from "./optimizable-image-hosts.mjs";

// Re-exported from the plain `.mjs` source of truth (also imported by next.config.mjs) so
// remotePatterns and the runtime check below share one list. See ./optimizable-image-hosts.mjs.
export { OPTIMIZABLE_IMAGE_HOSTS };

const OPTIMIZABLE_IMAGE_HOSTS_SET: ReadonlySet<string> = new Set(OPTIMIZABLE_IMAGE_HOSTS);
const LOOPBACK_HOSTS_SET: ReadonlySet<string> = new Set(LOOPBACK_HOSTS);

// Must mirror the protocol `next.config.mjs` pins per host in `images.remotePatterns`: `http` for
// loopback hosts, `https` for every other allowlisted (public) host. Next.js matches `remotePatterns`
// protocol strictly, so a src whose protocol disagrees would be rejected by the optimizer even though
// its hostname is allowlisted.
const optimizableProtocolFor = (hostname: string): string =>
LOOPBACK_HOSTS_SET.has(hostname) ? "http:" : "https:";

/**
* Whether an `<Image>` src must be rendered with `unoptimized` because the Next.js image optimizer
Expand All @@ -14,11 +22,14 @@ const OPTIMIZABLE_IMAGE_HOSTS_SET: ReadonlySet<string> = new Set(OPTIMIZABLE_IMA
* Returns `false` (i.e. optimize) for:
* - relative paths (`/storage/...`, `/images/...`) — local images, optimized via `localPatterns`;
* - `data:` URIs, `StaticImageData` imports, or empty/nullish values;
* - absolute URLs whose host is in {@link OPTIMIZABLE_IMAGE_HOSTS}.
* - absolute URLs whose host is in {@link OPTIMIZABLE_IMAGE_HOSTS} AND whose protocol matches what
* `next.config.mjs` generates for that host in `remotePatterns` (`http` for loopback, `https`
* otherwise).
*
* Returns `true` (i.e. bypass the optimizer, serve directly) for any other absolute `http(s)` URL —
* i.e. arbitrary user-provided external images. This keeps the optimizer from acting as an open
* proxy for hosts we don't control, without breaking rendering of those images.
* i.e. arbitrary user-provided external images, or an allowlisted host requested over the "wrong"
* protocol (which the optimizer would reject with a 400 anyway). This keeps the optimizer from acting
* as an open proxy for hosts we don't control, without breaking rendering of those images.
*
* The decision depends only on the src string, so it is identical on the server and client (no
* hydration mismatch).
Expand All @@ -27,7 +38,10 @@ export const isExternalImageSrc = (src: string | StaticImageData | null | undefi
if (!src || typeof src !== "string") return false;
if (!/^https?:\/\//i.test(src)) return false; // relative path or data: URI → local/optimizable
try {
return !OPTIMIZABLE_IMAGE_HOSTS_SET.has(new URL(src).hostname);
const url = new URL(src);
const isOptimizableHost = OPTIMIZABLE_IMAGE_HOSTS_SET.has(url.hostname);
const hasOptimizableProtocol = url.protocol === optimizableProtocolFor(url.hostname);
return !(isOptimizableHost && hasOptimizableProtocol);
} catch {
return false;
}
Expand Down
9 changes: 7 additions & 2 deletions apps/web/lib/optimizable-image-hosts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
* It therefore contains only *universal* provider hosts that real features rely on and that are
* identical on every deployment.
*/
// Loopback hosts are only ever reachable over plain HTTP (local development); every other
// allowlisted host is a public provider only ever reachable over HTTPS. Exported so both
// `next.config.mjs` (remotePatterns protocol) and `lib/image-hosts.ts` (runtime protocol check)
// agree on which hosts are loopback instead of each keeping their own copy.
export const LOOPBACK_HOSTS = ["localhost", "127.0.0.1"];

export const OPTIMIZABLE_IMAGE_HOSTS = [
// OAuth profile avatars
"avatars.githubusercontent.com",
Expand All @@ -23,6 +29,5 @@ export const OPTIMIZABLE_IMAGE_HOSTS = [
// survey editor's Unsplash background picker
"images.unsplash.com",
// local development
"localhost",
"127.0.0.1",
...LOOPBACK_HOSTS,
];
4 changes: 2 additions & 2 deletions apps/web/lib/turbo-build-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { describe, expect, test } from "vitest";

// Guards the coupling between next.config.mjs and turbo.json (see ENG-1663): every env var read in
// next.config.mjs shapes the build output, so it must be part of Turborepo's cache key. A var that
// is missing from `build.env` (or filed under `passThroughEnv`) makes Turborepo — including the CI
// remote cache — replay stale builds when the var's value changes.
// is missing from `build.env` (or filed under `passThroughEnv`) makes Turborepo — the local cache
// and the CI build-output cache alike — replay stale builds when the var's value changes.

const here = path.dirname(fileURLToPath(import.meta.url));
const nextConfigPath = path.resolve(here, "..", "next.config.mjs");
Expand Down
Loading
Loading