Files
immich-folio/lib/env.ts
Ralksta a3a6319c29 release: v0.16.0
See CHANGELOG.md for the full v0.16.0 entry.

---------

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>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
2026-09-22 14:12:13 +02:00

147 lines
6.2 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;
}
/**
* Strip a trailing slash (or several) from a URL, after confirming it parses
* at all. Deliberately not `/api`-aware, unlike install.ts's
* normalizeApiBase(): this is the "base URL, no /api" shape getConfig()
* expects (it appends /api itself unless the value already ends with one),
* and it's what the install wizard's URL field pre-fills from — appending
* /api here would mean re-opening the wizard shows a path nobody typed.
*
* Exported so lib/install.ts can apply the same normalisation to
* content/install.json's apiUrl, which used to skip it entirely: a trailing
* slash there produced IMMICH_API_URL//api and every request 404ed (#634).
*/
export function normalizeApiUrl(raw: string): string {
try {
return new URL(raw).toString().replace(/\/+$/, '');
} catch {
return '';
}
}
function parseEnv(): Env {
const urlRaw = process.env.IMMICH_API_URL;
let apiUrl = '';
if (urlRaw) {
apiUrl = normalizeApiUrl(urlRaw);
if (!apiUrl) {
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();