feat(privacy): add option to disable all external connections (#146)

* docs: add references to PUB_DISABLE_ALL_EXTERNAL_REQUESTS

* fix: somehow i forgot the PUB_ prefix

* feat: remove all external stuff (stripe, plausible, video) when PUB_DISABLE_ALL_EXTERNAL_REQUESTS is on

* docs: add note

* fix(docs): grammar

* fix: only hide plausible opt-in when privacy mode is enabled
This commit is contained in:
nullptr 2025-10-19 19:17:32 +01:00 committed by GitHub
parent 9bae3620ce
commit 6273b56360
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 268 additions and 200 deletions

View File

@ -2,6 +2,7 @@ PUB_HOSTNAME=localhost:5173 # only gets used for plausible (for now)
PUB_PLAUSIBLE_URL=https://plausible.example.com # can be empty
PUB_ENV=development # "production", "development", or "nightly"
PUB_VERTD_URL=https://vertd.vert.sh # default vertd instance
PUB_DISABLE_ALL_EXTERNAL_REQUESTS=false # disables vertd, stripe, plausible, etc. use if your boss complains about privacy
# please do not change these. donations help a lot
PUB_DONATION_URL=https://donations.vert.sh

View File

@ -1,67 +1,68 @@
name: Docker Image CI
on:
push:
branches: [ "main" ]
tags: [ 'v*' ]
paths:
- 'src/**'
- 'static/**'
pull_request:
branches: [ "main" ]
paths:
- 'src/**'
- 'static/**'
workflow_dispatch:
push:
branches: ["main"]
tags: ["v*"]
paths:
- "src/**"
- "static/**"
pull_request:
branches: ["main"]
paths:
- "src/**"
- "static/**"
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
PUB_ENV=production
PUB_HOSTNAME=${{ vars.PUB_HOSTNAME || '' }}
PUB_PLAUSIBLE_URL=${{ vars.PUB_PLAUSIBLE_URL || '' }}
PUB_VERTD_URL=https://vertd.vert.sh
PUB_DONATION_URL=https://donations.vert.sh
PUB_STRIPE_KEY=pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
PUB_ENV=production
PUB_HOSTNAME=${{ vars.PUB_HOSTNAME || '' }}
PUB_PLAUSIBLE_URL=${{ vars.PUB_PLAUSIBLE_URL || '' }}
PUB_VERTD_URL=https://vertd.vert.sh
PUB_DISABLE_ALL_EXTERNAL_REQUESTS=false
PUB_DONATION_URL=https://donations.vert.sh
PUB_STRIPE_KEY=pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2

View File

@ -6,6 +6,7 @@ ARG PUB_ENV
ARG PUB_HOSTNAME
ARG PUB_PLAUSIBLE_URL
ARG PUB_VERTD_URL
ARG PUB_DISABLE_ALL_EXTERNAL_REQUESTS
ARG PUB_DONATION_URL
ARG PUB_STRIPE_KEY
@ -13,6 +14,7 @@ ENV PUB_ENV=${PUB_ENV}
ENV PUB_HOSTNAME=${PUB_HOSTNAME}
ENV PUB_PLAUSIBLE_URL=${PUB_PLAUSIBLE_URL}
ENV PUB_VERTD_URL=${PUB_VERTD_URL}
ENV PUB_DISABLE_ALL_EXTERNAL_REQUESTS=${PUB_DISABLE_ALL_EXTERNAL_REQUESTS}
ENV PUB_DONATION_URL=${PUB_DONATION_URL}
ENV PUB_STRIPE_KEY=${PUB_STRIPE_KEY}
@ -33,4 +35,4 @@ COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/build /usr/share/nginx/html
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl --fail --silent --output /dev/null http://localhost || exit 1
CMD curl --fail --silent --output /dev/null http://localhost || exit 1

View File

@ -8,6 +8,7 @@ services:
PUB_HOSTNAME: ${PUB_HOSTNAME:-localhost:5173}
PUB_PLAUSIBLE_URL: ${PUB_PLAUSIBLE_URL:-}
PUB_ENV: ${PUB_ENV:-production}
PUB_DISABLE_ALL_EXTERNAL_REQUESTS: ${DISABLE_ALL_EXTERNAL_REQUESTS:-false}
PUB_VERTD_URL: ${PUB_VERTD_URL:-}
PUB_DONATION_URL: ${PUB_DONATION_URL:-https://donations.vert.sh}
PUB_STRIPE_KEY: ${PUB_STRIPE_KEY:-pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2}

View File

@ -6,13 +6,16 @@ This file covers how to run VERT under a Docker container.
- [Using an image from the GitHub Container Registry](#using-an-image-from-the-github-container-registry)
### Manually building the image
First, clone the repository:
```shell
$ git clone https://github.com/VERT-sh/VERT
$ cd VERT/
```
Then build a Docker image with:
```shell
$ docker build -t vert-sh/vert \
--build-arg PUB_ENV=production \
@ -20,10 +23,12 @@ $ docker build -t vert-sh/vert \
--build-arg PUB_PLAUSIBLE_URL=https://plausible.example.com \
--build-arg PUB_VERTD_URL=https://vertd.vert.sh \
--build-arg PUB_DONATION_URL=https://donations.vert.sh \
--build-arg PUB_DISABLE_ALL_EXTERNAL_REQUESTS=false
--build-arg PUB_STRIPE_KEY="" .
```
You can then run it by using:
```shell
$ docker run -d \
--restart unless-stopped \
@ -33,6 +38,7 @@ $ docker run -d \
```
This will do the following:
- Use the previously built image as the container `vert`, in detached mode
- Continuously restart the container until manually stopped
- Map `3000/tcp` (host) to `80/tcp` (container)
@ -40,7 +46,9 @@ This will do the following:
We also have a [`docker-compose.yml`](/docker-compose.yml) file available. Use `docker compose up` if you want to start the stack, or `docker compose down` to bring it down. You can pass `--build` to `docker compose up` to rebuild the Docker image (useful if you've changed any of the environment variables) as well as `-d` to start it in detached mode. You can read more about Docker Compose in general [here](https://docs.docker.com/compose/intro/compose-application-model/).
### Using an image from the GitHub Container Registry
While there's an image you can pull instead of cloning the repo and building the image yourself, you will not be able to update any of the environment variables (e.g. `PUB_PLAUSIBLE_URL`) as they're baked directly into the image and not obtained during runtime. If you're okay with this, you can simply run this command instead:
```shell
$ docker run -d \
--restart unless-stopped \

View File

@ -1,4 +1,4 @@
import { PUB_ENV } from "$env/static/public";
import { PUB_DISABLE_ALL_EXTERNAL_REQUESTS, PUB_ENV } from "$env/static/public";
export const GITHUB_URL_VERT = "https://github.com/VERT-sh/VERT";
export const GITHUB_URL_VERTD = "https://github.com/VERT-sh/vertd";
@ -11,3 +11,7 @@ export const VERT_NAME =
? "VERT Nightly"
: "VERT.sh";
export const CONTACT_EMAIL = "hello@vert.sh";
// i'm not entirely sure this should be in consts.ts, but it is technically a constant as .env is static for VERT
export const DISABLE_ALL_EXTERNAL_REQUESTS =
PUB_DISABLE_ALL_EXTERNAL_REQUESTS === "true";

View File

@ -4,13 +4,23 @@ import { FFmpegConverter } from "./ffmpeg.svelte";
import { PandocConverter } from "./pandoc.svelte";
import { VertdConverter } from "./vertd.svelte";
import { MagickConverter } from "./magick.svelte";
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/consts";
export const converters = [
new MagickConverter(),
new FFmpegConverter(),
new VertdConverter(),
new PandocConverter(),
];
const getConverters = (): Converter[] => {
const converters: Converter[] = [
new MagickConverter(),
new FFmpegConverter(),
];
if (!DISABLE_ALL_EXTERNAL_REQUESTS) {
converters.push(new VertdConverter());
}
converters.push(new PandocConverter());
return converters;
};
export const converters = getConverters();
export function getConverterByFormat(format: string) {
for (const converter of converters) {

View File

@ -1,7 +1,10 @@
<script lang="ts">
import Panel from "$lib/components/visual/Panel.svelte";
import { HeartHandshakeIcon } from "lucide-svelte";
import { GITHUB_URL_VERT } from "$lib/consts";
import {
DISABLE_ALL_EXTERNAL_REQUESTS,
GITHUB_URL_VERT,
} from "$lib/consts";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
@ -90,48 +93,50 @@
</div>
<!-- GitHub contributors -->
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h2 class="text-base font-bold">
{m["about.credits.github_contributors"]()}
</h2>
{#if ghContribs && ghContribs.length > 0}
<p class="text-base text-muted font-normal">
{@html link(
["jpegify_link", "github_link"],
m["about.credits.github_description"](),
["/jpegify", GITHUB_URL_VERT],
[false, true],
[
"text-black dynadark:text-white",
"text-blue-500 font-normal hover:underline",
],
)}
</p>
{:else}
<p class="text-base text-muted font-normal italic">
{@html link(
"contribute_link",
m["about.credits.no_contributors"](),
GITHUB_URL_VERT,
)}
</p>
{/if}
</div>
{#if ghContribs && ghContribs.length > 0}
<div class="flex flex-row flex-wrap gap-2">
{#each ghContribs as contrib}
{@const { name, github, avatar } = contrib}
{@render contributor(name, github, avatar)}
{/each}
{#if !DISABLE_ALL_EXTERNAL_REQUESTS}
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<h2 class="text-base font-bold">
{m["about.credits.github_contributors"]()}
</h2>
{#if ghContribs && ghContribs.length > 0}
<p class="text-base text-muted font-normal">
{@html link(
["jpegify_link", "github_link"],
m["about.credits.github_description"](),
["/jpegify", GITHUB_URL_VERT],
[false, true],
[
"text-black dynadark:text-white",
"text-blue-500 font-normal hover:underline",
],
)}
</p>
{:else}
<p class="text-base text-muted font-normal italic">
{@html link(
"contribute_link",
m["about.credits.no_contributors"](),
GITHUB_URL_VERT,
)}
</p>
{/if}
</div>
{/if}
<h2 class="mt-2 -mb-2">{m["about.credits.libraries"]()}</h2>
<p class="font-normal">
{m["about.credits.libraries_description"]()}
</p>
</div>
{#if ghContribs && ghContribs.length > 0}
<div class="flex flex-row flex-wrap gap-2">
{#each ghContribs as contrib}
{@const { name, github, avatar } = contrib}
{@render contributor(name, github, avatar)}
{/each}
</div>
{/if}
<h2 class="mt-2 -mb-2">{m["about.credits.libraries"]()}</h2>
<p class="font-normal">
{m["about.credits.libraries_description"]()}
</p>
</div>
{/if}
</div></Panel
>

View File

@ -15,6 +15,7 @@
import { onMount } from "svelte";
import { error } from "$lib/logger";
import { ToastManager } from "$lib/toast/index.svelte";
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/consts";
const { settings = $bindable() }: { settings: ISettings } = $props();
@ -81,50 +82,55 @@
{m["settings.privacy.title"]()}
</h2>
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">
{m["settings.privacy.plausible_title"]()}
</p>
<p class="text-sm text-muted font-normal">
{@html link(
["plausible_link", "analytics_link"],
m["settings.privacy.plausible_description"](),
[
"https://plausible.io/privacy-focused-web-analytics",
"https://ats.vert.sh/vert.sh",
],
)}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
<div class="flex gap-3 w-full">
<button
onclick={() => (settings.plausible = true)}
class="btn {$effects
? ''
: '!scale-100'} {settings.plausible
? 'selected'
: ''} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PlayIcon size="24" class="inline-block mr-2" />
{m["settings.privacy.opt_in"]()}
</button>
{#if !DISABLE_ALL_EXTERNAL_REQUESTS}
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">
{m["settings.privacy.plausible_title"]()}
</p>
<p class="text-sm text-muted font-normal">
{@html link(
["plausible_link", "analytics_link"],
m["settings.privacy.plausible_description"](),
[
"https://plausible.io/privacy-focused-web-analytics",
"https://ats.vert.sh/vert.sh",
],
)}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
<div class="flex gap-3 w-full">
<button
onclick={() => (settings.plausible = true)}
class="btn {$effects
? ''
: '!scale-100'} {settings.plausible
? 'selected'
: ''} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PlayIcon size="24" class="inline-block mr-2" />
{m["settings.privacy.opt_in"]()}
</button>
<button
onclick={() => (settings.plausible = false)}
class="btn {$effects
? ''
: '!scale-100'} {settings.plausible
? ''
: 'selected'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PauseIcon size="24" class="inline-block mr-2" />
{m["settings.privacy.opt_out"]()}
</button>
<button
onclick={() => (settings.plausible = false)}
class="btn {$effects
? ''
: '!scale-100'} {settings.plausible
? ''
: 'selected'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PauseIcon
size="24"
class="inline-block mr-2"
/>
{m["settings.privacy.opt_out"]()}
</button>
</div>
</div>
</div>
</div>
{/if}
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">

View File

@ -3,7 +3,7 @@
import { goto, beforeNavigate, afterNavigate } from "$app/navigation";
import { PUB_PLAUSIBLE_URL, PUB_HOSTNAME } from "$env/static/public";
import { VERT_NAME } from "$lib/consts";
import { DISABLE_ALL_EXTERNAL_REQUESTS, VERT_NAME } from "$lib/consts";
import * as Layout from "$lib/components/layout";
import * as Navbar from "$lib/components/layout/Navbar";
import featuredImage from "$lib/assets/VERT_Feature.webp";
@ -94,12 +94,14 @@
Settings.instance.load();
VertdInstance.instance
.url()
.then((u) => fetch(`${u}/api/version`))
.then((res) => {
if (res.ok) $vertdLoaded = true;
});
if (!DISABLE_ALL_EXTERNAL_REQUESTS) {
VertdInstance.instance
.url()
.then((u) => fetch(`${u}/api/version`))
.then((res) => {
if (res.ok) $vertdLoaded = true;
});
}
return () => {
window.removeEventListener("paste", handlePaste);
@ -109,7 +111,9 @@
$effect(() => {
enablePlausible =
!!PUB_PLAUSIBLE_URL && Settings.instance.settings.plausible;
!!PUB_PLAUSIBLE_URL &&
Settings.instance.settings.plausible &&
!DISABLE_ALL_EXTERNAL_REQUESTS;
if (!enablePlausible && browser) {
// reset pushState on opt-out so that plausible stops firing events on page navigation
history.pushState = History.prototype.pushState;
@ -128,7 +132,7 @@
name="description"
content="With VERT you can quickly convert any image, video and audio file. No ads, no tracking, open source, and all processing (other than video) is done on your device."
/>
<meta property="og:url" content="https://vert.sh">
<meta property="og:url" content="https://vert.sh" />
<meta property="og:type" content="website" />
<meta
property="og:title"
@ -139,9 +143,9 @@
content="With VERT you can quickly convert any image, video and audio file. No ads, no tracking, open source, and all processing (other than video) is done on your device."
/>
<meta property="og:image" content={featuredImage} />
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="vert.sh">
<meta property="twitter:url" content="https://vert.sh">
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="vert.sh" />
<meta property="twitter:url" content="https://vert.sh" />
<meta
property="twitter:title"
content="{VERT_NAME} — Free, fast, and awesome file converter"

View File

@ -11,6 +11,7 @@
import "overlayscrollbars/overlayscrollbars.css";
import { onMount } from "svelte";
import type { WorkerStatus } from "$lib/converters/converter.svelte";
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/consts";
const getSupportedFormats = (name: string) =>
converters
@ -28,37 +29,51 @@
title: string;
status: WorkerStatus;
};
} = $derived({
Images: {
formats: getSupportedFormats("imagemagick"),
icon: Image,
title: m["upload.cards.images"](),
status:
converters.find((c) => c.name === "imagemagick")?.status ||
"not-ready",
},
Audio: {
formats: getSupportedFormats("ffmpeg"),
icon: AudioLines,
title: m["upload.cards.audio"](),
status:
converters.find((c) => c.name === "ffmpeg")?.status ||
"not-ready",
},
Documents: {
formats: getSupportedFormats("pandoc"),
icon: BookText,
title: m["upload.cards.documents"](),
status:
converters.find((c) => c.name === "pandoc")?.status ||
"not-ready",
},
Video: {
formats: getSupportedFormats("vertd"),
icon: Film,
title: m["upload.cards.video"](),
status: $vertdLoaded === true ? "ready" : "not-ready", // not using converter.status for this
},
} = $derived.by(() => {
const output: {
[key: string]: {
formats: string;
icon: typeof Image;
title: string;
status: WorkerStatus;
};
} = {
Images: {
formats: getSupportedFormats("imagemagick"),
icon: Image,
title: m["upload.cards.images"](),
status:
converters.find((c) => c.name === "imagemagick")?.status ||
"not-ready",
},
Audio: {
formats: getSupportedFormats("ffmpeg"),
icon: AudioLines,
title: m["upload.cards.audio"](),
status:
converters.find((c) => c.name === "ffmpeg")?.status ||
"not-ready",
},
Documents: {
formats: getSupportedFormats("pandoc"),
icon: BookText,
title: m["upload.cards.documents"](),
status:
converters.find((c) => c.name === "pandoc")?.status ||
"not-ready",
},
};
if (!DISABLE_ALL_EXTERNAL_REQUESTS) {
output.Video = {
formats: getSupportedFormats("vertd"),
icon: Film,
title: m["upload.cards.video"](),
status: $vertdLoaded === true ? "ready" : "not-ready", // not using converter.status for this
};
}
return output;
});
const getTooltip = (format: string) => {

View File

@ -8,7 +8,7 @@
import avatarJovannMC from "$lib/assets/avatars/jovannmc.jpg";
import avatarRealmy from "$lib/assets/avatars/realmy.jpg";
import avatarAzurejelly from "$lib/assets/avatars/azurejelly.jpg";
import { GITHUB_API_URL } from "$lib/consts";
import { DISABLE_ALL_EXTERNAL_REQUESTS, GITHUB_API_URL } from "$lib/consts";
import { dev } from "$app/environment";
import { page } from "$app/state";
import { m } from "$lib/paraglide/messages";
@ -70,6 +70,10 @@
let ghContribs: Contributor[] = [];
onMount(async () => {
if (DISABLE_ALL_EXTERNAL_REQUESTS) {
return;
}
// Check if the data is already in sessionStorage
const cachedContribs = sessionStorage.getItem("ghContribs");
if (cachedContribs) {
@ -134,7 +138,9 @@
}
});
const donationsEnabled = dev || page.url.origin.endsWith("//vert.sh");
const donationsEnabled =
(dev || page.url.origin.endsWith("//vert.sh")) &&
!DISABLE_ALL_EXTERNAL_REQUESTS;
</script>
<div class="flex flex-col h-full items-center">

View File

@ -7,6 +7,7 @@
import { onMount } from "svelte";
import { m } from "$lib/paraglide/messages";
import { ToastManager } from "$lib/toast/index.svelte";
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/consts";
let settings = $state(Settings.Settings.instance.settings);
@ -63,12 +64,16 @@
>
<div class="flex flex-col gap-4 flex-1">
<Settings.Conversion bind:settings />
<Settings.Vertd bind:settings />
{#if !DISABLE_ALL_EXTERNAL_REQUESTS}
<Settings.Vertd bind:settings />
{:else if PUB_PLAUSIBLE_URL}
<Settings.Privacy bind:settings />
{/if}
</div>
<div class="flex flex-col gap-4 flex-1">
<Settings.Appearance />
{#if PUB_PLAUSIBLE_URL}
{#if PUB_PLAUSIBLE_URL && !DISABLE_ALL_EXTERNAL_REQUESTS}
<Settings.Privacy bind:settings />
{/if}
</div>