Files
immich-folio/lib/env.ts
Ralksta a813433891 release: v0.13.0 (#535)
Twenty-nine commits since v0.12.0. See CHANGELOG.md for the full list
and the upgrade notes.

Ten additions — the SEO package (site URL, robots.txt, sitemap.xml,
JSON-LD), slideshow mode, opt-in original downloads, copy-link,
previous/next album navigation, alt text from the Immich description,
per-album location precision, the update notice, a Help tab, and a
subpage cover grid separate from its photo grid. Two fixes: CJK album
slugs, and the lightbox control bar.

Photo of the Day (#476) was built on `dev` and reverted as not planned;
it never appears in a release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01DmsHVRL9yQvckUB1ZpVWre

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:08:12 +02:00

128 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Environment variables parser.
* Parsed once at startup — all env access should go through this module.
*/
export interface Env {
IMMICH_API_URL: string;
IMMICH_API_KEY: string;
SITE_TITLE: string;
SITE_SUBTITLE: string;
CACHE_TTL: number;
STALE_MAX_AGE: number;
IMAGE_CACHE_VERSION: string;
IMMICH_TIMEOUT_MS: number;
RATE_LIMIT_RPM: number;
AUTH_SECRET?: string;
TRUSTED_PROXY_HOPS: number;
WEBHOOK_SECRET?: string;
ADMIN_PASSWORD?: string;
SITE_PASSWORD?: string;
INSTALL_CONTENT_DIR?: string;
/** Whether the admin dashboard may ask GitHub for the latest release. */
UPDATE_CHECK: boolean;
/** Absolute site URL for sitemap, feed and JSON-LD. Overrides settings.yaml. */
SITE_URL?: string;
}
function parseEnv(): Env {
const urlRaw = process.env.IMMICH_API_URL;
let apiUrl = '';
if (urlRaw) {
try {
apiUrl = new URL(urlRaw).toString().replace(/\/+$/, '');
} catch {
console.warn('⚠️ IMMICH_API_URL is invalid, falling back to empty string for setup.');
}
}
const apiKey = process.env.IMMICH_API_KEY || '';
const cacheTtlStr = process.env.CACHE_TTL;
const cacheTtl =
cacheTtlStr && !isNaN(parseInt(cacheTtlStr, 10)) ? parseInt(cacheTtlStr, 10) : 300;
// How long an expired cache entry may still be served while Immich is
// unreachable. Keeps the public gallery online across an Immich restart
// instead of showing an error page. 0 disables the fallback.
const staleMaxAgeStr = process.env.STALE_MAX_AGE;
const staleMaxAge =
staleMaxAgeStr && !isNaN(parseInt(staleMaxAgeStr, 10)) ? parseInt(staleMaxAgeStr, 10) : 86400; // 24 hours
// How long to wait on Immich before giving up. Without an explicit budget the
// only ceiling is undici's built-in default (minutes), and since every page is
// force-dynamic an Immich that accepts connections but never answers would
// hold each visitor's render open for that entire window.
//
// This bounds the wait for a *response*, not the transfer of an image or video
// body — those are legitimately slow and are not capped by this value.
const timeoutStr = process.env.IMMICH_TIMEOUT_MS;
const timeoutMs =
timeoutStr && !isNaN(parseInt(timeoutStr, 10)) ? parseInt(timeoutStr, 10) : 15000;
// Manual cache-buster for image and video URLs. Empty by default, so normal
// operation is untouched.
//
// Image responses are served `immutable` for a year, which means the browser
// does not revalidate at all — not even the ETag is consulted. That is the
// right default (these URLs are content-addressed by an encrypted asset ID),
// but it leaves no way to push a changed rendition: Immich regenerates
// thumbnails and previews when a job is re-run or a photo is rotated, and the
// asset ID — hence the token, hence the URL — stays the same. Bumping this
// value changes every image URL at once and is the only escape hatch short of
// rotating AUTH_SECRET.
const imageCacheVersion = (process.env.IMAGE_CACHE_VERSION || '').trim();
const rateLimitStr = process.env.RATE_LIMIT_RPM;
const rateLimit =
rateLimitStr && !isNaN(parseInt(rateLimitStr, 10)) ? parseInt(rateLimitStr, 10) : 1500;
// Number of reverse proxies in front of the app. Determines how far from the
// right of X-Forwarded-For the real client IP sits. nginx/Traefik/Caddy = 1;
// add 1 for each additional layer (e.g. Cloudflare in front of nginx = 2).
const hopsStr = process.env.TRUSTED_PROXY_HOPS;
let trustedProxyHops = hopsStr && !isNaN(parseInt(hopsStr, 10)) ? parseInt(hopsStr, 10) : 0;
// TRUSTED_PROXIES (removed) matched proxy IPs against the socket peer address,
// which self-hosted Next.js never exposes — so it silently disabled per-client
// rate limiting and lumped every visitor into one shared bucket. Migrate the
// common single-proxy case rather than leaving those deploys broken.
if (trustedProxyHops === 0 && process.env.TRUSTED_PROXIES) {
trustedProxyHops = 1;
console.warn(
'\n⚠️ TRUSTED_PROXIES is deprecated and has no effect. Assuming TRUSTED_PROXY_HOPS=1.\n' +
' Set TRUSTED_PROXY_HOPS explicitly (nginx/Traefik/Caddy = 1) and remove TRUSTED_PROXIES.\n',
);
}
if (trustedProxyHops < 0) trustedProxyHops = 0;
return {
IMMICH_API_URL: apiUrl,
IMMICH_API_KEY: apiKey as string,
SITE_TITLE: process.env.SITE_TITLE || 'Gallery',
SITE_SUBTITLE: process.env.SITE_SUBTITLE || '',
CACHE_TTL: Math.max(0, cacheTtl),
STALE_MAX_AGE: Math.max(0, staleMaxAge),
// Kept URL-safe: this value is appended to every image and video URL.
IMAGE_CACHE_VERSION: encodeURIComponent(imageCacheVersion),
// A sub-second budget would abort healthy requests; 0 must not mean "no timeout".
IMMICH_TIMEOUT_MS: Math.max(1000, timeoutMs),
RATE_LIMIT_RPM: Math.max(1, rateLimit),
AUTH_SECRET: process.env.AUTH_SECRET,
TRUSTED_PROXY_HOPS: trustedProxyHops,
WEBHOOK_SECRET: process.env.WEBHOOK_SECRET || undefined,
ADMIN_PASSWORD: process.env.ADMIN_PASSWORD || undefined,
SITE_PASSWORD: process.env.SITE_PASSWORD || undefined,
INSTALL_CONTENT_DIR: process.env.INSTALL_CONTENT_DIR || undefined,
// On unless switched off: an instance that never learns about a security
// release is the worse default. Only `false` disables it, so a typo does
// not silently turn the check off.
UPDATE_CHECK: process.env.UPDATE_CHECK !== 'false',
SITE_URL: process.env.SITE_URL || undefined,
};
}
/** Validated, typed environment variables. */
export const env = parseEnv();