v3: comments say what the code can't — the documentation pass wanted JSDoc on every export and every field of an exported interface, which filled the code with restatements ("The dump.", "Whether it is loading.", "The props of X.", "Renders the X.") and module docs that told a reader nothing ("The hot and new feeds, and the props every main feed of the index takes."); some were wrong as well as vague. Every comment in api/, shared/ and src/ is rewritten under one rule, now stated in the README's "Comments" section: every module and export still has a JSDoc, and it gives context (who uses it and where), usage (what a caller must know: a precondition, what it leaves to the caller, a side effect) or its place in the chain (where its input comes from, where its output goes); a field, parameter, private function or local gets one only when it adds to its parent's, and context about a whole type goes on the type; a type's doc is phrased from the value's side ("Returned to Y by X when Z", "Passed to Y by X"); full sentences, as short as they can be, no label-colon-list; // goes wherever the code isn't obvious and says why, and the section banners and narration go. Every claim was checked against the code, which turned up some forty comments that were wrong: DumpCard's claimed it was only used by the hot and new feeds, the WebSocket welcome claimed to be a socket's first message (presence comes first), UpdateUserRequest said null removes a description (it keeps it), deletedDumpIds leaves out that it also holds dumps made private, THEME_COLOR claimed to mirror index.html's, and the like. The routers' JSDoc now says where they are mounted and what becomes of a thrown APIException. doc-lint.ts lets a field without a doc through; a module or export without one still fails. No code changes; 204 backend and 628 frontend tests pass, and doc:lint, deno check, deno lint and tsc are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-29 14:31:14 +00:00
parent 42e0356d21
commit 0d4e344d77
287 changed files with 5679 additions and 5135 deletions

View File

@@ -1,6 +1,7 @@
/**
* The server's settings: those read from `GERBEUR_*` environment variables,
* with their defaults, and the fixed limits the backend applies.
* Reads the server's settings from the `GERBEUR_*` environment variables, 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
*/
@@ -8,12 +9,13 @@
/** The port the server listens on (`GERBEUR_PORT`, default 8000). */
export const PORT: number = Number(Deno.env.get("GERBEUR_PORT")) || 8000;
/**
* The SMTP server mail is sent through, as an `smtps://` URL
* (`GERBEUR_SMTPS_URL`). Empty disables email.
* The `smtps://` URL of the server email-service sends mail through
* (`GERBEUR_SMTPS_URL`). When it is empty, the welcome and password reset
* emails are silently skipped.
*/
export const SMTPS_URL: string = Deno.env.get("GERBEUR_SMTPS_URL")?.trim() ||
"";
/** The sender address of outgoing mail (`GERBEUR_FROM_EMAIL`). */
/** The `From` address of the welcome and password reset emails. */
export const FROM_EMAIL: string = Deno.env.get("GERBEUR_FROM_EMAIL")?.trim() ||
"";
@@ -24,66 +26,76 @@ Hi **{{username}}**,
Your account has been created successfully. Welcome aboard!`;
/**
* The Markdown body of the welcome email (`GERBEUR_WELCOME_EMAIL_BODY`), with
* `{{site_name}}` and `{{username}}` placeholders.
* The Markdown of the welcome email sent on registration
* (`GERBEUR_WELCOME_EMAIL_BODY`). The users route replaces `{{site_name}}` and
* `{{username}}` in it and sends it as both text and HTML.
*/
export const WELCOME_EMAIL_BODY: string =
Deno.env.get("GERBEUR_WELCOME_EMAIL_BODY")?.trim() ||
DEFAULT_WELCOME_EMAIL_BODY;
/** The key tokens are signed with (`GERBEUR_JWT_SECRET`). Required. */
/**
* 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 network interface the server binds to (`GERBEUR_LISTEN_HOST`). Defaults
* to 0.0.0.0 so Docker port-forwarding works out of the box; 127.0.0.1
* restricts it to loopback.
* 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 server's public-facing URL (`GERBEUR_PUBLIC_URL`), without a trailing
* slash — used for CORS, WebSocket origin checks, email links, and OG image
* URLs. Behind a reverse proxy, set it to the externally-visible URL (e.g.
* https://example.com). Defaults to http://localhost:PORT for local
* development.
* 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}`;
/**
* The frontend's URL (`GERBEUR_FRONTEND_URL`). In single-container deployments
* the API serves the frontend, so it defaults to {@link PUBLIC_URL}; set it
* when the frontend runs on a separate host or port (e.g. Vite dev server or a
* CDN origin).
* The URL the frontend is served from (`GERBEUR_FRONTEND_URL`), when it isn't
* {@link PUBLIC_URL}, as with a separate Vite server. It is an allowed origin,
* and password reset links point to it.
*/
export const FRONTEND_URL: string =
Deno.env.get("GERBEUR_FRONTEND_URL")?.trim() || PUBLIC_URL;
/**
* The SQLite database file (`GERBEUR_DB_PATH`). Overridable so the test suite
* can run against a throwaway database (see api/test/setup.ts).
* 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";
// Upload/files
/**
* The directory uploads are stored under (`GERBEUR_UPLOADS_DIR`), one
* subdirectory per kind.
* 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";
/** Where file dumps are stored. */
/** Holds the file of each file dump. */
export const DUMPS_DIR = `${UPLOADS_DIR}/dumps`;
/** Where avatars are stored. */
/** Holds each user's avatar. */
export const AVATARS_DIR = `${UPLOADS_DIR}/avatars`;
/** Where playlist cover images are stored. */
/** Holds each playlist's cover. */
export const PLAYLIST_IMAGES_DIR = `${UPLOADS_DIR}/playlist-images`;
/** Where images pasted into chat, comments and descriptions are stored. */
/**
* 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`;
/** Where custom dump thumbnails are stored. */
/**
* Holds the thumbnails a poster uploads (`<id>-custom`) and those the
* thumbnails route generates from file dumps.
*/
export const THUMBNAILS_DIR = `${UPLOADS_DIR}/thumbnails`;
/** The largest image upload accepted: 5 MB. */
/**
* The largest image upload accepted, in bytes. lib/upload.ts's error message
* says "5 MB", so the two change together.
*/
export const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024;
/** The image types accepted for avatars, covers, thumbnails and attachments. */
export const ALLOWED_IMAGE_MIMES: Set<string> = new Set([
@@ -93,14 +105,17 @@ export const ALLOWED_IMAGE_MIMES: Set<string> = new Set([
"image/webp",
]);
/** The MIME type prefixes a file dump may have. */
/**
* 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 exact MIME types a file dump may have, beyond the prefixes. */
/** The documents and archives a file dump may be, beyond the prefixes. */
export const DUMP_ALLOWED_MIME_TYPES: Set<string> = new Set([
"application/pdf",
"application/json",
@@ -114,41 +129,44 @@ export const DUMP_ALLOWED_MIME_TYPES: Set<string> = new Set([
"application/vnd.ms-powerpoint",
]);
// Pagination
/** 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;
// Startup housekeeping
/** How long an attachment nothing links to is kept before startup purges it. */
export const ORPHANED_ATTACHMENTS_RETENTION_HOURS = 1;
/** How long an unused invite is kept before startup purges it. */
/**
* 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;
// SEO/OG
/** The site's name (`GERBEUR_SITE_NAME`), in page titles, OG tags and mail. */
/**
* 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 the favicon set and app header logo are generated from
* (`GERBEUR_SITE_EMOJI`). Applied at runtime (server startup and index.html
* injection), so changing it only needs a restart — no rebuild.
* 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") || "🚚";
/**
* A Bandcamp playback path.
*
* - `"embed"` — the bandcamp.com iframe (cannot autoplay)
* - `"native"` — resolve the page's mp3 streams and play them in the app's own
* player, which autoplays and supports albums as a queue
* 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";
/**
* The Bandcamp playback path the frontend uses (`GERBEUR_BANDCAMP_PLAYER`).
* Anything unrecognised falls back to `"embed"`, so a typo degrades safely.
* 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"
@@ -156,21 +174,21 @@ export const BANDCAMP_PLAYER: BandcampPlayer =
: "embed";
/**
* The background color of generated icons and the manifest. Mirrors the
* hard-coded theme-color in index.html.
* 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 CORS and the WebSocket accept: {@link PUBLIC_URL},
* {@link FRONTEND_URL} and the comma-separated `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 is auto-included so the separate-frontend case only needs
// GERBEUR_FRONTEND_URL — no need to repeat it in GERBEUR_ALLOWED_ORIGINS.
FRONTEND_URL,
...rawOrigins.split(",").map((o) => o.trim()).filter(Boolean),
]),