server/env.ts
Rama main · réplica de lectura del repositorio original.
/* oxlint-disable no-console */
import environment from "./utils/environment";
import os from "node:os";
import wellKnownServices from "nodemailer/lib/well-known/services.json";
import {
validate,
IsNotEmpty,
IsUrl,
IsOptional,
IsHexadecimal,
Length,
IsNumber,
IsIn,
IsBoolean,
Min,
} from "class-validator";
import { uniq } from "es-toolkit/compat";
import { languages } from "@shared/i18n";
import { Day, Hour } from "@shared/utils/time";
import {
CannotUseWith,
CannotUseWithout,
CannotUseWithAny,
IsInCaseInsensitive,
IsDatabaseUrl,
IsMailboxAddress,
} from "@server/utils/validators";
import Deprecated from "./models/decorators/Deprecated";
import { getArg } from "./utils/args";
import { Public, PublicEnvironmentRegister } from "./utils/decorators/Public";
export class Environment {
constructor() {
process.nextTick(() => {
void validate(this).then((errors) => {
if (errors.length > 0) {
let output =
"Environment configuration is invalid, please check the following:\n\n";
output += errors.map(
(error) => "- " + Object.values(error.constraints ?? {}).join(", ")
);
console.warn(output);
process.exit(1);
}
});
});
PublicEnvironmentRegister.registerEnv(this);
}
/**
* Returns an object consisting of env vars annotated with `@Public` decorator
*/
get public() {
return PublicEnvironmentRegister.getEnv();
}
/**
* The current environment name.
*/
@Public
@IsIn(["development", "production", "staging", "test"])
public ENVIRONMENT = environment.NODE_ENV ?? "production";
/**
* The secret key is used for encrypting data. Do not change this value once
* set or your users will be unable to login.
*/
@IsHexadecimal()
@Length(64, 64, {
message: `The SECRET_KEY environment variable must be exactly 64 hexadecimal characters (Use \`openssl rand -hex 32\` to generate a value).`,
})
public SECRET_KEY = environment.SECRET_KEY ?? "";
/**
* The secret that should be passed to the cron utility endpoint to enable
* triggering of scheduled tasks.
*/
@IsNotEmpty()
public UTILS_SECRET = environment.UTILS_SECRET ?? "";
/**
* The url of the database.
*/
@IsOptional()
@IsDatabaseUrl()
@CannotUseWithAny([
"DATABASE_HOST",
"DATABASE_PORT",
"DATABASE_NAME",
"DATABASE_USER",
"DATABASE_PASSWORD",
])
public DATABASE_URL = this.toOptionalString(environment.DATABASE_URL);
/**
* Optional database URL for read replica to distribute read queries
* and reduce load on primary database.
*/
@IsOptional()
@IsDatabaseUrl()
public DATABASE_READ_ONLY_URL = this.toOptionalString(
// Support deprecated variable name for backwards compatibility
environment.DATABASE_READ_ONLY_URL ?? environment.DATABASE_URL_READ_ONLY
);
/**
* Database host for individual component configuration.
*/
@IsOptional()
@CannotUseWith("DATABASE_URL")
public DATABASE_HOST = this.toOptionalString(environment.DATABASE_HOST);
/**
* Database port for individual component configuration.
*/
@IsOptional()
@IsNumber()
@CannotUseWith("DATABASE_URL")
public DATABASE_PORT = this.toOptionalNumber(environment.DATABASE_PORT);
/**
* Database name for individual component configuration.
*/
@IsOptional()
@CannotUseWith("DATABASE_URL")
public DATABASE_NAME = this.toOptionalString(environment.DATABASE_NAME);
/**
* Database user for individual component configuration.
*/
@IsOptional()
@CannotUseWith("DATABASE_URL")
public DATABASE_USER = this.toOptionalString(environment.DATABASE_USER);
/**
* Database password for individual component configuration.
*/
@IsOptional()
@CannotUseWith("DATABASE_URL")
public DATABASE_PASSWORD = this.toOptionalString(
environment.DATABASE_PASSWORD
);
/**
* An optional database schema.
*/
@IsOptional()
public DATABASE_SCHEMA = this.toOptionalString(environment.DATABASE_SCHEMA);
/**
* The url of the database pool.
*/
@IsOptional()
@IsDatabaseUrl()
public DATABASE_CONNECTION_POOL_URL = this.toOptionalString(
environment.DATABASE_CONNECTION_POOL_URL
);
/**
* Database connection pool configuration.
*/
@IsNumber()
@IsOptional()
public DATABASE_CONNECTION_POOL_MIN = this.toOptionalNumber(
environment.DATABASE_CONNECTION_POOL_MIN
);
/**
* Database connection pool configuration. At least two connections are
* needed, as some operations query outside of a transaction they hold open.
*/
@IsNumber()
@IsOptional()
@Min(2)
public DATABASE_CONNECTION_POOL_MAX = this.toOptionalNumber(
environment.DATABASE_CONNECTION_POOL_MAX
);
/**
* Set to "disable" to disable SSL connection to the database. This option is
* passed through to Postgres. See:
*
* https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
*/
@IsIn(["disable", "allow", "require", "prefer", "verify-ca", "verify-full"])
@IsOptional()
public PGSSLMODE = environment.PGSSLMODE;
/**
* The url of redis. Note that redis does not have a database after the port.
* Note: More extensive validation isn't included here due to our support for
* base64-encoded configuration.
*/
@IsNotEmpty()
public REDIS_URL = environment.REDIS_URL;
/**
* The url of redis for horizontally scaling the collaboration service. If not
* set then the collaboration service must be ran as a singleton.
*/
public REDIS_COLLABORATION_URL = environment.REDIS_COLLABORATION_URL;
/**
* The interval in milliseconds between redis connection healthchecks. Each
* healthcheck issues a PING and forces a reconnect if it fails.
*/
@IsNumber()
@Min(1000)
public REDIS_HEALTHCHECK_INTERVAL = parseInt(
environment.REDIS_HEALTHCHECK_INTERVAL || "30000",
10
);
/**
* The timeout in milliseconds for a redis healthcheck PING before the
* connection is considered stuck and forcibly reconnected.
*/
@IsNumber()
@Min(100)
public REDIS_HEALTHCHECK_TIMEOUT = parseInt(
environment.REDIS_HEALTHCHECK_TIMEOUT || "5000",
10
);
/**
* The time in milliseconds that a redis connection can stay unavailable
* before the process exits, so that the process manager can restart it.
*/
@IsNumber()
@Min(1000)
public REDIS_UNAVAILABLE_TIMEOUT = parseInt(
environment.REDIS_UNAVAILABLE_TIMEOUT || "300000",
10
);
/**
* The timeout in milliseconds for a command on the redis connection used for
* ephemeral data such as caches, locks and rate limits, after which the
* command fails.
*/
@IsNumber()
@Min(100)
public REDIS_EPHEMERAL_COMMAND_TIMEOUT = parseInt(
environment.REDIS_EPHEMERAL_COMMAND_TIMEOUT || "500",
10
);
/**
* The fully qualified, external facing domain name of the server.
* If not set, will be derived from HEROKU_APP_NAME for Heroku deployments.
*/
@Public
@IsNotEmpty()
@IsUrl({
protocols: ["http", "https"],
require_protocol: true,
require_tld: false,
})
public URL = (
environment.URL ??
(environment.HEROKU_APP_NAME
? `https://${environment.HEROKU_APP_NAME}.herokuapp.com`
: "")
).replace(/\/$/, "");
/**
* If using a Cloudfront/Cloudflare distribution or similar it can be set below.
* This will cause paths to javascript, stylesheets, and images to be updated to
* the hostname defined in CDN_URL. In your CDN configuration the origin server
* should be set to the same as URL.
*/
@Public
@IsOptional()
@IsUrl({
protocols: ["http", "https"],
require_protocol: true,
require_tld: false,
})
public CDN_URL = this.toOptionalString(
environment.CDN_URL ? environment.CDN_URL.replace(/\/$/, "") : undefined
);
/**
* The fully qualified, external facing domain name of the collaboration
* service, if different (unlikely)
*/
@Public
@IsUrl({
require_tld: false,
require_protocol: true,
protocols: ["http", "https", "ws", "wss"],
})
@IsOptional()
public COLLABORATION_URL = (environment.COLLABORATION_URL || this.URL)
.replace(/\/$/, "")
.replace(/^http/, "ws");
/**
* The maximum number of network clients that can be connected to a single
* document at once. Defaults to 100.
*/
@IsOptional()
@IsNumber()
public COLLABORATION_MAX_CLIENTS_PER_DOCUMENT = parseInt(
environment.COLLABORATION_MAX_CLIENTS_PER_DOCUMENT || "100",
10
);
/**
* The port that the server will listen on, defaults to 3000.
*/
@IsNumber()
@IsOptional()
public PORT = this.toOptionalNumber(environment.PORT) ?? 3000;
/**
* Optional extra debugging. Comma separated
*/
public DEBUG = environment.DEBUG || "";
/**
* Configure lowest severity level for server logs
*/
@IsIn(["error", "warn", "info", "http", "verbose", "debug", "silly"])
public LOG_LEVEL = environment.LOG_LEVEL || "info";
/**
* How many processes should be spawned. As a reasonable rule divide your
* server's available memory by 512 for a rough estimate
*/
@IsNumber()
@IsOptional()
public WEB_CONCURRENCY = this.toOptionalNumber(environment.WEB_CONCURRENCY);
/**
* How long a request should be processed before giving up and returning an
* error response to the client, defaults to 10s
*/
@IsNumber()
@IsOptional()
public REQUEST_TIMEOUT =
this.toOptionalNumber(environment.REQUEST_TIMEOUT) ?? 10 * 1000;
/**
* Base64 encoded protected key if Outline is to perform SSL termination.
*/
@IsOptional()
@CannotUseWithout("SSL_CERT")
public SSL_KEY = this.toOptionalString(environment.SSL_KEY);
/**
* Base64 encoded public certificate if Outline is to perform SSL termination.
*/
@IsOptional()
@CannotUseWithout("SSL_KEY")
public SSL_CERT = this.toOptionalString(environment.SSL_CERT);
/**
* The default interface language. See translate.getoutline.com for a list of
* available language codes and their percentage translated.
*/
@Public
@IsIn(languages)
public DEFAULT_LANGUAGE = environment.DEFAULT_LANGUAGE ?? "en_US";
/**
* A comma list of which services should be enabled on this instance – defaults to all.
* The "admin" service is only available in development.
*
* If a services flag is passed it takes priority over the environment variable
* for example: --services=web,worker
*/
public SERVICES = uniq(
(
getArg("services") ??
environment.SERVICES ??
"collaboration,websockets,worker,web"
)
.split(",")
.map((service) => service.toLowerCase().trim())
);
/**
* Auto-redirect to https in production. The default is true but you may set
* to false if you can be sure that SSL is terminated at an external
* loadbalancer.
*/
@IsBoolean()
public FORCE_HTTPS = this.toBoolean(environment.FORCE_HTTPS ?? "true");
/**
* When the app is behind a proxy, sets the HTTP header used for the client IP.
* The default value is "X-Forwarded-For", common values are "X-Real-IP"
* and "X-Client-IP".
*/
@IsOptional()
public PROXY_IP_HEADER = this.toOptionalString(environment.PROXY_IP_HEADER);
/**
* Whether to trust the X-Forwarded-* headers (e.g. X-Forwarded-For,
* X-Forwarded-Proto) set by an upstream proxy or load balancer. Defaults to
* true for backwards compat. Set to false if not running behind a proxy in production.
*/
@IsBoolean()
public PROXY_HEADERS_TRUSTED = this.toBoolean(
environment.PROXY_HEADERS_TRUSTED ?? "true"
);
/**
* Should the installation send anonymized statistics to the maintainers.
* Defaults to true.
*/
@IsBoolean()
public TELEMETRY = this.toBoolean(
environment.ENABLE_UPDATES ?? environment.TELEMETRY ?? "true"
);
// Third-party services
/**
* The host of your SMTP server for enabling emails.
*/
@CannotUseWith("SMTP_SERVICE")
public SMTP_HOST = this.toOptionalString(environment.SMTP_HOST);
/**
* The service name of a well-known SMTP service for nodemailer.
* See https://community.nodemailer.com/2-0-0-beta/setup-smtp/well-known-services/
*/
@CannotUseWith("SMTP_HOST")
@IsInCaseInsensitive(Object.keys(wellKnownServices))
public SMTP_SERVICE = this.toOptionalString(environment.SMTP_SERVICE);
@Public
public EMAIL_ENABLED =
!!(this.SMTP_HOST || this.SMTP_SERVICE) || this.isDevelopment;
/**
* Optional hostname of the client, used for identifying to the server
* defaults to hostname of the machine.
*/
public SMTP_NAME = environment.SMTP_NAME;
/**
* The port of your SMTP server.
*/
@IsNumber()
@IsOptional()
@CannotUseWith("SMTP_SERVICE")
public SMTP_PORT = this.toOptionalNumber(environment.SMTP_PORT);
/**
* The username of your SMTP server, if any.
*/
public SMTP_USERNAME = environment.SMTP_USERNAME;
/**
* The password for the SMTP username, if any.
*/
public SMTP_PASSWORD = environment.SMTP_PASSWORD;
/**
* The email address from which emails are sent.
*/
@IsMailboxAddress()
@IsOptional()
public SMTP_FROM_EMAIL = this.toOptionalString(environment.SMTP_FROM_EMAIL);
/**
* The reply-to address for emails sent from Outline. If unset the from
* address is used by default.
*/
@IsMailboxAddress()
@IsOptional()
public SMTP_REPLY_EMAIL = this.toOptionalString(environment.SMTP_REPLY_EMAIL);
/**
* Override the cipher used for SMTP SSL connections.
*/
public SMTP_TLS_CIPHERS = this.toOptionalString(environment.SMTP_TLS_CIPHERS);
/**
* If true (the default) the connection will use TLS when connecting to server.
* If false then TLS is used only if server supports the STARTTLS extension.
*
* Setting secure to false therefore does not mean that you would not use an
* encrypted connection.
*/
public SMTP_SECURE = this.toBoolean(environment.SMTP_SECURE ?? "true");
/**
* If true then STARTTLS is disabled even if the server supports it.
* If false (the default) then STARTTLS is used if server supports it.
*
* Setting secure to false therefore does not mean that you would not use an
* encrypted connection.
*/
public SMTP_DISABLE_STARTTLS = this.toBoolean(
environment.SMTP_DISABLE_STARTTLS ?? "false"
);
/**
* Dropbox app key for embedding Dropbox files
*/
@Public
@IsOptional()
public DROPBOX_APP_KEY = this.toOptionalString(environment.DROPBOX_APP_KEY);
/**
* Sentry DSN for capturing errors and frontend performance.
*/
@Public
@IsUrl()
@IsOptional()
public SENTRY_DSN = this.toOptionalString(environment.SENTRY_DSN);
/**
* Sentry tunnel URL for bypassing ad blockers
*/
@Public
@IsUrl()
@IsOptional()
public SENTRY_TUNNEL = this.toOptionalString(environment.SENTRY_TUNNEL);
/**
* A release SHA or other identifier for Sentry.
*/
public RELEASE = this.toOptionalString(environment.RELEASE);
/**
* A Google Analytics tracking ID, supports v3 or v4 properties.
*/
@Public
@IsOptional()
public GOOGLE_ANALYTICS_ID = this.toOptionalString(
environment.GOOGLE_ANALYTICS_ID
);
/**
* A DataDog API key for tracking server metrics.
*/
public DD_API_KEY = environment.DD_API_KEY;
/**
* The name of the service to use in DataDog.
*/
public DD_SERVICE = environment.DD_SERVICE ?? "outline";
/**
* A string representing the version of the software.
*
* SOURCE_COMMIT is used by Docker Hub
* SOURCE_VERSION is used by Heroku
*/
@Public
public VERSION = this.toOptionalString(
environment.SOURCE_COMMIT || environment.SOURCE_VERSION
);
/**
* The maximum number of concurrent events processed per-worker. To get total
* concurrency you should multiply this by the number of workers.
*/
@IsOptional()
@IsNumber()
public WORKER_CONCURRENCY_EVENTS =
this.toOptionalNumber(environment.WORKER_CONCURRENCY_EVENTS) ?? 10;
/**
* The maximum number of concurrent tasks processed per-worker. To get total
* concurrency you should multiply this by the number of workers.
*/
@IsOptional()
@IsNumber()
public WORKER_CONCURRENCY_TASKS =
this.toOptionalNumber(environment.WORKER_CONCURRENCY_TASKS) ?? 10;
/**
* A boolean switch to toggle the rate limiter at application web server.
*/
@IsOptional()
@IsBoolean()
public RATE_LIMITER_ENABLED = this.toBoolean(
environment.RATE_LIMITER_ENABLED ?? "true"
);
/**
* Set max allowed requests in a given duration for default rate limiter to
* trigger throttling, per IP address.
*/
@IsOptional()
@IsNumber()
@CannotUseWithout("RATE_LIMITER_ENABLED")
public RATE_LIMITER_REQUESTS =
this.toOptionalNumber(environment.RATE_LIMITER_REQUESTS) ?? 1000;
/**
* Set max allowed realtime connections before throttling. Defaults to 50
* requests/ip/duration window.
*/
@IsOptional()
@IsNumber()
public RATE_LIMITER_COLLABORATION_REQUESTS =
this.toOptionalNumber(environment.RATE_LIMITER_COLLABORATION_REQUESTS) ??
50;
/**
* Set fixed duration window(in secs) for default rate limiter, elapsing which
* the request quota is reset (the bucket is refilled with tokens).
*/
@IsOptional()
@IsNumber()
@CannotUseWithout("RATE_LIMITER_ENABLED")
public RATE_LIMITER_DURATION_WINDOW =
this.toOptionalNumber(environment.RATE_LIMITER_DURATION_WINDOW) ?? 60;
/**
* Multiplier applied to the per-endpoint API rate limits. Allows operators to
* uniformly scale the hard-coded route-level limits up or down without
* touching code. A value of 1 (the default) preserves the built-in limits.
* Effective per-endpoint limits are scaled by this value, rounded to the
* nearest integer, and clamped to a minimum of 1.
*/
@IsOptional()
@IsNumber()
@Min(0)
@CannotUseWithout("RATE_LIMITER_ENABLED")
public RATE_LIMITER_MULTIPLIER =
this.toOptionalFloat(environment.RATE_LIMITER_MULTIPLIER) ?? 1;
/**
* Set max allowed upload size for file attachments.
* @deprecated Use FILE_STORAGE_UPLOAD_MAX_SIZE instead
*/
@IsOptional()
@IsNumber()
@Deprecated("Use FILE_STORAGE_UPLOAD_MAX_SIZE instead")
public AWS_S3_UPLOAD_MAX_SIZE = this.toOptionalNumber(
environment.AWS_S3_UPLOAD_MAX_SIZE
);
/**
* Access key ID for AWS S3.
*/
@IsOptional()
public AWS_ACCESS_KEY_ID = this.toOptionalString(
environment.AWS_ACCESS_KEY_ID
);
/**
* Secret key for AWS S3.
*/
@IsOptional()
@CannotUseWithout("AWS_ACCESS_KEY_ID")
public AWS_SECRET_ACCESS_KEY = this.toOptionalString(
environment.AWS_SECRET_ACCESS_KEY
);
/**
* The name of the AWS S3 region to use.
*/
@IsOptional()
public AWS_REGION = this.toOptionalString(environment.AWS_REGION);
/**
* Optional AWS S3 endpoint URL for file attachments.
*/
@Public
@IsOptional()
public AWS_S3_ACCELERATE_URL = environment.AWS_S3_ACCELERATE_URL ?? "";
/**
* Optional CloudFront distribution URL for serving attachment downloads.
* Uploads continue to use the S3 endpoint directly. When set together with
* AWS_CLOUDFRONT_KEY_PAIR_ID and a private key, signed CloudFront URLs are
* used for downloads. If signing credentials are missing, S3 presigned URLs
* are used instead.
* Example: https://d1a2b3c4d5e6f.cloudfront.net (no trailing slash)
*/
@IsOptional()
public AWS_CLOUDFRONT_URL = this.toOptionalString(
environment.AWS_CLOUDFRONT_URL
);
/**
* CloudFront key pair ID for signed download URLs. Required together with a
* private key when AWS_CLOUDFRONT_URL is set.
*/
@IsOptional()
public AWS_CLOUDFRONT_KEY_PAIR_ID = this.toOptionalString(
environment.AWS_CLOUDFRONT_KEY_PAIR_ID
);
/**
* PEM-encoded RSA private key for CloudFront signed URLs, or a base64-encoded
* PEM string on a single line. Use a YAML block scalar in docker-compose for
* multi-line PEM values.
*/
@IsOptional()
public AWS_CLOUDFRONT_PRIVATE_KEY = this.toOptionalString(
environment.AWS_CLOUDFRONT_PRIVATE_KEY
);
/**
* Optional AWS S3 endpoint URL for file attachments.
*/
@Public
@IsOptional()
public AWS_S3_UPLOAD_BUCKET_URL = environment.AWS_S3_UPLOAD_BUCKET_URL ?? "";
/**
* The bucket name to store file attachments in.
*/
@IsOptional()
public AWS_S3_UPLOAD_BUCKET_NAME = this.toOptionalString(
environment.AWS_S3_UPLOAD_BUCKET_NAME
);
/**
* Whether to force path style URLs for S3 objects, this is required for some
* S3-compatible storage providers.
*/
@IsOptional()
public AWS_S3_FORCE_PATH_STYLE = this.toBoolean(
environment.AWS_S3_FORCE_PATH_STYLE ?? "true"
);
/**
* Set default AWS S3 ACL for file attachments.
*/
@IsOptional()
public AWS_S3_ACL = environment.AWS_S3_ACL ?? "private";
/**
* Which HTTP method to use for presigned uploads to S3-compatible storage.
* "post" uses multipart form upload (traditional S3 presigned POST).
* "put" uses a single PUT request with a presigned URL (required for
* providers like Cloudflare R2 that do not support presigned POST).
*/
@IsIn(["put", "post"])
public AWS_S3_UPLOAD_METHOD =
this.toOptionalString(environment.AWS_S3_UPLOAD_METHOD) ?? "post";
/**
* Which file storage system to use
*/
@IsIn(["local", "s3"])
public FILE_STORAGE = this.toOptionalString(environment.FILE_STORAGE) ?? "s3";
/**
* Set default root dir path for local file storage
*/
public FILE_STORAGE_LOCAL_ROOT_DIR =
this.toOptionalString(environment.FILE_STORAGE_LOCAL_ROOT_DIR) ??
"/var/lib/outline/data";
/**
* Set max allowed upload size for file attachments.
*/
@IsNumber()
public FILE_STORAGE_UPLOAD_MAX_SIZE =
this.toOptionalNumber(environment.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
this.toOptionalNumber(environment.AWS_S3_UPLOAD_MAX_SIZE) ??
1000000;
/**
* Set max allowed upload size for document imports.
*/
@Public
@IsNumber()
public FILE_STORAGE_IMPORT_MAX_SIZE =
this.toOptionalNumber(environment.FILE_STORAGE_IMPORT_MAX_SIZE) ??
this.toOptionalNumber(environment.MAXIMUM_IMPORT_SIZE) ??
this.toOptionalNumber(environment.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
1000000;
/**
* Timeout in milliseconds for downloading files from remote locations to file storage.
*/
@IsNumber()
public FILE_STORAGE_IMPORT_TIMEOUT =
this.toOptionalNumber(environment.FILE_STORAGE_IMPORT_TIMEOUT) ?? 60000;
/**
* Set max allowed upload size for imports at workspace level.
*/
@IsNumber()
public FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE =
this.toOptionalNumber(environment.FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE) ??
this.toOptionalNumber(environment.MAXIMUM_IMPORT_SIZE) ??
this.toOptionalNumber(environment.FILE_STORAGE_UPLOAD_MAX_SIZE) ??
1000000;
/**
* Because imports can be much larger than regular file attachments and are
* deleted automatically we allow an optional separate limit on the size of
* imports.
*
* @deprecated Use `FILE_STORAGE_IMPORT_MAX_SIZE` or `FILE_STORAGE_WORKSPACE_IMPORT_MAX_SIZE` instead
*/
@IsOptional()
@IsNumber()
@Deprecated("Use FILE_STORAGE_IMPORT_MAX_SIZE instead")
public MAXIMUM_IMPORT_SIZE = this.toOptionalNumber(
environment.MAXIMUM_IMPORT_SIZE
);
/**
* Limit on export size in bytes. Defaults to the total memory available to
* the container.
*/
@IsNumber()
public MAXIMUM_EXPORT_SIZE =
this.toOptionalNumber(environment.MAXIMUM_EXPORT_SIZE) ?? os.totalmem();
/**
* The number of seconds access tokens issue by the OAuth provider are valid.
*/
@IsNumber()
public OAUTH_PROVIDER_ACCESS_TOKEN_LIFETIME =
this.toOptionalNumber(environment.OAUTH_PROVIDER_ACCESS_TOKEN_LIFETIME) ??
Hour.seconds;
/**
* The number of seconds refresh tokens issue by the OAuth provider are valid.
*/
@IsNumber()
public OAUTH_PROVIDER_REFRESH_TOKEN_LIFETIME =
this.toOptionalNumber(environment.OAUTH_PROVIDER_REFRESH_TOKEN_LIFETIME) ??
30 * Day.seconds;
/**
* The number of seconds authorization codes issue by the OAuth provider are valid.
*/
@IsNumber()
public OAUTH_PROVIDER_AUTHORIZATION_CODE_LIFETIME =
this.toOptionalNumber(
environment.OAUTH_PROVIDER_AUTHORIZATION_CODE_LIFETIME
) ?? 300;
/**
* Whether to disable OAuth Dynamic Client Registration (DCR). When set to
* true, the POST /oauth/register endpoint will be unavailable.
*/
@IsBoolean()
public OAUTH_DISABLE_DCR = this.toBoolean(
environment.OAUTH_DISABLE_DCR ?? "false"
);
/**
* Whether to disable OAuth Client ID Metadata Documents (CIMD). When set to
* true, clients must use Dynamic Client Registration instead. Use this when
* the server cannot fetch metadata documents from client hosts.
*/
@IsBoolean()
public OAUTH_DISABLE_CIMD = this.toBoolean(
environment.OAUTH_DISABLE_CIMD ?? "false"
);
/**
* Enable unsafe-inline in script-src CSP directive
*/
@IsBoolean()
public DEVELOPMENT_UNSAFE_INLINE_CSP = this.toBoolean(
environment.DEVELOPMENT_UNSAFE_INLINE_CSP ?? "false"
);
/**
* Time window in seconds to analyze webhook failures for disabling decision.
* Defaults to 86400 seconds (24 hours).
*/
@IsNumber()
@IsOptional()
public WEBHOOK_FAILURE_TIME_WINDOW =
this.toOptionalNumber(environment.WEBHOOK_FAILURE_TIME_WINDOW) ?? 86400;
/**
* Percentage threshold of failures within the time window that triggers
* webhook disabling. Defaults to 80%.
*/
@IsNumber()
@IsOptional()
public WEBHOOK_FAILURE_RATE_THRESHOLD =
this.toOptionalNumber(environment.WEBHOOK_FAILURE_RATE_THRESHOLD) ?? 80;
/**
* Comma-separated list of IP addresses or CIDR ranges that are allowed to be accessed
* even if they are private IP addresses. This is useful for allowing
* connections to OIDC providers or webhooks on private networks.
* Supports both individual IP addresses and CIDR notation.
* Example: "10.0.0.1,192.168.1.0/24,172.16.0.1"
*/
@IsOptional()
public ALLOWED_PRIVATE_IP_ADDRESSES = this.toOptionalCommaList(
environment.ALLOWED_PRIVATE_IP_ADDRESSES
);
/**
* The search provider to use. Defaults to "postgres" which uses PostgreSQL
* full-text search. Alternative providers can be registered via plugins.
*/
@IsOptional()
public SEARCH_PROVIDER =
this.toOptionalString(environment.SEARCH_PROVIDER) ?? "postgres";
/**
* The product name
*/
@Public
public APP_NAME = "Outline";
/**
* Gravity constant for time decay in popularity scoring. Higher values cause
* faster decay of older content. Default is 0.7.
*/
@IsOptional()
@IsNumber()
public POPULARITY_GRAVITY =
this.toOptionalNumber(environment.POPULARITY_GRAVITY) ?? 0.7;
/**
* Number of weeks of activity to consider when calculating popularity scores.
* Default is 2 weeks.
*/
@IsOptional()
@IsNumber()
public POPULARITY_ACTIVITY_THRESHOLD_WEEKS =
this.toOptionalNumber(environment.POPULARITY_ACTIVITY_THRESHOLD_WEEKS) ?? 2;
/**
* Interval in hours at which popularity scores are recalculated.
* Default is 12 hours.
*/
@IsOptional()
@IsNumber()
public POPULARITY_UPDATE_INTERVAL_HOURS =
this.toOptionalNumber(environment.POPULARITY_UPDATE_INTERVAL_HOURS) ?? 12;
/**
* Returns true if the current installation is the cloud hosted version at
* getoutline.com
*/
public get isCloudHosted() {
return [
"https://app.getoutline.com",
"https://app.outline.dev",
"https://app.outline.dev:3000",
].includes(this.URL);
}
/**
* Returns true if the current installation is running in production.
*/
public get isProduction() {
return this.ENVIRONMENT === "production";
}
/**
* Returns true if the current installation is running in the development environment.
*/
public get isDevelopment() {
return this.ENVIRONMENT === "development";
}
/**
* Returns true if the current installation is running in a test environment.
*/
public get isTest() {
return this.ENVIRONMENT === "test";
}
/**
* Returns the port that the Vite development server listens on.
*/
public get vitePort() {
return this.PORT + 1;
}
protected toOptionalString(value: string | undefined) {
return value ? value : undefined;
}
protected toOptionalCommaList(value: string | undefined) {
return value ? value.split(",").map((item) => item.trim()) : undefined;
}
protected toOptionalNumber(value: string | undefined) {
return value ? parseInt(value, 10) : undefined;
}
protected toOptionalFloat(value: string | undefined) {
return value ? parseFloat(value) : undefined;
}
/**
* Convert a string to a boolean. Supports the following:
*
* 0 = false
* 1 = true
* "true" = true
* "false" = false
* "" = false
*
* @param value The string to convert
* @returns A boolean
*/
protected toBoolean(value: string) {
try {
return value ? !!JSON.parse(value) : false;
} catch (_err) {
throw new Error(
`"${value}" could not be parsed as a boolean, must be "true" or "false"`
);
}
}
/**
* Convert a string to an optional boolean. Supports the following:
*
* 0 = false
* 1 = true
* "true" = true
* "false" = false
* "" = undefined
*
* @param value The string to convert
* @returns A boolean or undefined
*/
protected toOptionalBoolean(value: string | undefined) {
try {
return value ? !!JSON.parse(value) : undefined;
} catch (_err) {
return undefined;
}
}
}
export default new Environment();