/** * Lets a deployment configure the server through the `GERBEUR_*` environment * variables, which it reads once at import, and holds the fixed limits the * backend applies. The deno tasks load `.env` with `--env-file`, so changing a * setting needs a restart. * * @module */ import { BASE_STYLE, isStyleName, type StyleName } from "../shared/theme.ts"; import { isLocale, type Locale, SOURCE_LOCALE } from "../shared/locale.ts"; /** The port the server listens on (`GERBEUR_PORT`, default 8000). */ export const PORT: number = Number(Deno.env.get("GERBEUR_PORT")) || 8000; /** * The `smtps://` URL of the server `email-service.ts` sends mail through * (`GERBEUR_SMTPS_URL`). When it is empty, no email is sent: sign-up sends no * welcome, a password reset request does nothing, and there are no digests. */ export const SMTPS_URL: string = Deno.env.get("GERBEUR_SMTPS_URL")?.trim() || ""; /** * Whether the instance sends email, which is whether {@link SMTPS_URL} is * set. `email-service.ts` opens its transport by it, and index.html tells the * client, whose settings show the digest switches disabled without it. */ export const EMAIL_ENABLED: boolean = SMTPS_URL.length > 0; /** * The `From` address of every email (`GERBEUR_FROM_EMAIL`), as an address or * as `Name
`. `email-service.ts` refuses to load without it when * {@link SMTPS_URL} is set. */ export const FROM_EMAIL: string = Deno.env.get("GERBEUR_FROM_EMAIL")?.trim() || ""; /** * The Markdown of the welcome email sent on registration * (`GERBEUR_WELCOME_EMAIL_BODY`), or null for the built-in one in the * {@link EMAIL_LOCALE}. `api/mail/welcome.ts` replaces `{{site_name}}` and * `{{username}}` in it. */ export const WELCOME_EMAIL_BODY: string | null = Deno.env.get("GERBEUR_WELCOME_EMAIL_BODY")?.trim() || null; /** * The language of every email (`GERBEUR_EMAIL_LOCALE`). The interface picks * each visitor's language in their browser, which the server never learns, so * the emails take the instance's. A value that isn't a locale means the * source one, English. */ export const EMAIL_LOCALE: Locale = (() => { const locale = Deno.env.get("GERBEUR_EMAIL_LOCALE")?.trim(); return isLocale(locale) ? locale : SOURCE_LOCALE; })(); /** * The instance's style (`GERBEUR_DEFAULT_STYLE`). The site shows it until a * visitor or their account picks one, through index.html, and the emails that * aren't a user's own (the welcome and the password reset) are drawn in it. A * value that isn't a style means {@link BASE_STYLE}. */ export const DEFAULT_STYLE: StyleName = (() => { const style = Deno.env.get("GERBEUR_DEFAULT_STYLE")?.trim(); return isStyleName(style) ? style : BASE_STYLE; })(); /** * The key lib/jwt.ts signs and verifies tokens with (`GERBEUR_JWT_SECRET`). * The server refuses to start without it. */ export const JWT_SECRET: string = Deno.env.get("GERBEUR_JWT_SECRET")?.trim() || ""; /** * The interface the server binds to (`GERBEUR_LISTEN_HOST`). The default, * 0.0.0.0, lets Docker forward the port; 127.0.0.1 keeps it to loopback. */ export const LISTEN_HOST: string = Deno.env.get("GERBEUR_LISTEN_HOST") || "0.0.0.0"; /** * The URL visitors reach the server at (`GERBEUR_PUBLIC_URL`), without a * trailing slash. Behind a reverse proxy it must be the outside URL: it is an * allowed origin, it tells links to this site's own pages from other links, * and it prefixes the image URLs in their previews. */ export const PUBLIC_URL: string = Deno.env.get("GERBEUR_PUBLIC_URL")?.trim().replace(/\/$/, "") || `http://localhost:${PORT}`; /** * Points password reset links and the Open Graph tags' page URLs at the * frontend when it isn't served from {@link PUBLIC_URL}, as with a separate * Vite server, and allows it as an origin. It is `GERBEUR_FRONTEND_URL`, * without a trailing slash. */ export const FRONTEND_URL: string = Deno.env.get("GERBEUR_FRONTEND_URL")?.trim().replace(/\/$/, "") || PUBLIC_URL; /** * The SQLite database file (`GERBEUR_DB_PATH`). api/test/setup.ts points it at * a throwaway file before anything opens the database. */ export const DB_PATH: string = Deno.env.get("GERBEUR_DB_PATH") || "api/sql/gerbeur.db"; /** * The directory uploads are stored under (`GERBEUR_UPLOADS_DIR`). Each kind has * its subdirectory below, created by the first upload, and each file is named * after the id of what it belongs to. */ export const UPLOADS_DIR: string = Deno.env.get("GERBEUR_UPLOADS_DIR") || "api/uploads"; /** Holds the file of each file dump. */ export const DUMPS_DIR = `${UPLOADS_DIR}/dumps`; /** Holds each user's avatar. */ export const AVATARS_DIR = `${UPLOADS_DIR}/avatars`; /** Holds each playlist's cover. */ export const PLAYLIST_IMAGES_DIR = `${UPLOADS_DIR}/playlist-images`; /** * Holds the images pasted into chat messages, comments, dump notes and * descriptions. The startup tasks purge those nothing links to. */ export const ATTACHMENTS_DIR = `${UPLOADS_DIR}/attachments`; /** * Holds the thumbnails a poster uploads (`-custom`) and those the * thumbnails route generates from file dumps. */ export const THUMBNAILS_DIR = `${UPLOADS_DIR}/thumbnails`; /** * Holds the scaled-down copies of the images above, which lib/ffmpeg.ts makes * as they are first asked for, each at its upload's path with its width. It * can be emptied at any time: the copies are made again. */ export const SCALED_DIR = `${UPLOADS_DIR}/scaled`; /** * Caps the size of an image upload, in bytes, for avatars, covers, thumbnails * and attachments. lib/upload.ts states it in megabytes in its error. */ export const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024; /** * The MIME type prefixes a file dump may have. dump-service also accepts the * exact types of {@link DUMP_ALLOWED_MIME_TYPES}. */ export const DUMP_ALLOWED_MIME_PREFIXES: string[] = [ "text/", "image/", "video/", "audio/", ]; /** The documents and archives a file dump may be, beyond the prefixes. */ export const DUMP_ALLOWED_MIME_TYPES: Set = new Set([ "application/pdf", "application/json", "application/zip", "application/x-zip-compressed", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", ]); /** The page size when a request names none. */ export const PAGINATION_DEFAULT_LIMIT = 20; /** The largest page size a request may ask for. */ export const PAGINATION_MAX_LIMIT = 100; /** How long an attachment nothing links to is kept before startup purges it. */ export const ORPHANED_ATTACHMENTS_RETENTION_HOURS = 1; /** * How long an invite stays usable. invite-service refuses an older one, and * the startup tasks purge it. */ export const UNUSED_INVITES_RETENTION_DAYS = 7; /** * How often, in minutes, the `/community` page's figures are recomputed * (`GERBEUR_COMMUNITY_REFRESH_MINUTES`), 15 by default. */ export const COMMUNITY_REFRESH_MINUTES: number = Number(Deno.env.get("GERBEUR_COMMUNITY_REFRESH_MINUTES")) || 15; /** * The site's name (`GERBEUR_SITE_NAME`). It is filled into index.html, the OG * tags, the icons' manifest and the emails. */ export const OG_SITE_NAME: string = Deno.env.get("GERBEUR_SITE_NAME") || "gerbeur"; /** * The emoji that serves as the site's logo (`GERBEUR_SITE_EMOJI`). The icons * script draws the favicons from it, and index.html carries it to the app's * header, so changing it needs a restart but no rebuild. */ export const SITE_EMOJI: string = Deno.env.get("GERBEUR_SITE_EMOJI") || "🚚"; /** * How the frontend plays Bandcamp. `"embed"` uses Bandcamp's iframe, which * can't autoplay. `"native"` resolves the page's streams through * `GET /api/bandcamp/tracks` and plays them in the app's player, which * autoplays and queues an album's tracks. */ export type BandcampPlayer = "embed" | "native"; /** * How the frontend plays Bandcamp (`GERBEUR_BANDCAMP_PLAYER`), passed to it in * index.html. Any value but `"native"` means `"embed"`, so a typo falls back to * the player that always works. */ export const BANDCAMP_PLAYER: BandcampPlayer = Deno.env.get("GERBEUR_BANDCAMP_PLAYER")?.trim() === "native" ? "native" : "embed"; /** * The background of the generated Apple touch icon, the manifest's colors, and * the accent of previews of this site's own pages. index.html's `theme-color` * tags don't follow it. */ export const THEME_COLOR = "#111827"; const rawOrigins = Deno.env.get("GERBEUR_ALLOWED_ORIGINS") ?? ""; /** * The origins that CORS in main.ts and the WebSocket route accept. They are * {@link PUBLIC_URL}, {@link FRONTEND_URL} and the comma-separated * `GERBEUR_ALLOWED_ORIGINS`. */ export const ALLOWED_ORIGINS: string[] = Array.from( new Set([ PUBLIC_URL, FRONTEND_URL, ...rawOrigins.split(",").map((o) => o.trim()).filter(Boolean), ]), ); /** * Lets federated sign-in reach instances on private addresses, `localhost` * among them, and over plain http (`GERBEUR_FEDERATION_ALLOW_PRIVATE=1`), so * that two instances on one machine can trust each other in development. * Off, `remote/gerbeur-instance.ts` reaches only public https instances, since * an identity is only as safe as the connection it came over. */ export const FEDERATION_ALLOW_PRIVATE: boolean = Deno.env.get("GERBEUR_FEDERATION_ALLOW_PRIVATE")?.trim() === "1"; /** * Which ways federated sign-in goes, each on unless set to `0`. * * - `incoming` (`GERBEUR_FEDERATION_INCOMING`): users of other instances sign * in here. Off, the login and register pages offer no other instance, * `/manage` lists only the blocked ones, which this instance's users can't * sign in to (and none with `outgoing` off too), `federation-service.ts` * refuses their sign-ins, and the accounts that came from one are signed * out (`getSessionState`) until it is on again. Their homes' events are * still applied, so that none is lost meanwhile. * - `outgoing` (`GERBEUR_FEDERATION_OUTGOING`): this instance's users sign in * to others. Off, `identity-provider-service.ts` lets no instance sign them * in, and its metadata says so, which the others check before sending * anyone here to sign in. The settings page lists no instances, since at * startup `main.ts` withdraws every grant they gave, which signs them out * there. * * index.html tells the client both. An object, so that the tests can turn one * off for a while. */ export const FEDERATION: { incoming: boolean; outgoing: boolean } = { incoming: Deno.env.get("GERBEUR_FEDERATION_INCOMING")?.trim() !== "0", outgoing: Deno.env.get("GERBEUR_FEDERATION_OUTGOING")?.trim() !== "0", };