docs: move the reference site to astro starlight (#2527)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-06 15:40:22 +02:00
1 parent 7f8f09ee51
commit 5367c0ab42
198 files changed
+59927 -1216

No files matched your search

+667
View File
@@ -0,0 +1,667 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {lstat} from 'node:fs/promises';
import {createServer, type IncomingMessage, type Server, type ServerResponse} from 'node:http';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import send from 'send';
import {type AstroServerShutdown, AstroServerTermination} from './AstroServerTermination';
import {
CanonicalNetworkProtocol,
getNodeErrorCode,
HTTPHeader,
HTTPMethod,
HTTPStatusCode,
isCanonicalHTTPNetworkProtocol,
MIMEType,
NodeErrorCode,
OUTBOUND_USER_AGENT,
} from './HTTPConstants';
import {siteSecurityHeaders} from './SecurityHeaders';
const HEALTH_PATH = '/_health';
const HEALTH_RESPONSE_BODY = JSON.stringify({status: 'ok'});
const NOT_READY_RESPONSE_BODY = JSON.stringify({status: 'not_ready'});
const WELL_KNOWN_PATH_PREFIX = '.well-known/';
const IMMUTABLE_ASSET_PATH_PREFIX = '_astro/';
const PARENT_SEGMENT_PATTERN = /(?:^|[\\/])\.\.(?:[\\/]|$)/u;
const IMMUTABLE_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
const LOCAL_REQUEST_URL_BASE = `${CanonicalNetworkProtocol.HTTP}//localhost`;
const ASTRO_SERVER_SHUTDOWN_TIMEOUT_MS = 15_000;
const ASTRO_SERVER_NAME = 'Fluxer Astro server';
const ASTRO_SERVER_MAX_CONNECTIONS = 256;
const ASTRO_SERVER_MAX_HEADERS = 64;
const ASTRO_SERVER_MAX_HEADER_BYTES = 16 * 1024;
const ASTRO_SERVER_MAX_REQUESTS_PER_SOCKET = 100;
const ASTRO_SERVER_MAX_ACTIVE_REQUESTS = ASTRO_SERVER_MAX_CONNECTIONS;
const ASTRO_SERVER_REQUEST_TARGET_MAX_BYTES = 8 * 1024;
const ASTRO_SERVER_HEADERS_TIMEOUT_MS = 10_000;
const ASTRO_SERVER_REQUEST_TIMEOUT_MS = 15_000;
const ASTRO_SERVER_KEEP_ALIVE_TIMEOUT_MS = 5_000;
const ASTRO_SERVER_SOCKET_TIMEOUT_MS = 30_000;
const ALLOWED_SITE_METHODS = `${HTTPMethod.GET}, ${HTTPMethod.HEAD}`;
const NO_STORE_CACHE_CONTROL = 'no-store';
const TEXT_CONTENT_TYPE = `${MIMEType.PLAIN}; charset=utf-8`;
const AstroServerStartupPhase = Object.freeze({
ENTRYPOINT: 'entrypoint load',
INITIALIZE: 'initialization',
LISTEN: 'listener bind',
} as const);
type AstroServerStartupPhase = (typeof AstroServerStartupPhase)[keyof typeof AstroServerStartupPhase];
const StaticDotfilePolicy = Object.freeze({
ALLOW: 'allow',
DENY: 'deny',
} as const);
type StaticDotfilePolicy = (typeof StaticDotfilePolicy)[keyof typeof StaticDotfilePolicy];
type AstroRequestNext = (error: unknown) => void;
type AstroRequestHandler = (
request: IncomingMessage,
response: ServerResponse,
next: AstroRequestNext,
locals: object,
) => void | Promise<void>;
interface AstroServerEntrypoint {
readonly handler: AstroRequestHandler;
}
interface StaticRequestPath {
readonly pathname: string;
readonly search: string;
readonly staticPath: string;
}
interface ResolvedStaticFile {
readonly isDirectory: boolean;
readonly normalizedPath: string;
}
interface ResolvedPublicEndpoint {
readonly basePath: string;
readonly secureOrigin: boolean;
}
type AstroServerInitialize = () => Promise<void>;
type AstroServerReadiness = () => boolean;
type AstroServerRequestLocals = () => object;
interface AstroServerOptions {
readonly entrypoint: URL;
readonly initialize: AstroServerInitialize | null;
readonly listenHost: string;
readonly listenPort: number;
readonly publicEndpoint: string;
readonly readiness: AstroServerReadiness | null;
readonly requestLocals: AstroServerRequestLocals | null;
readonly shutdown: AstroServerShutdown | null;
}
interface AstroServerHealthcheckOptions {
readonly listenHost: string;
readonly listenPort: number;
readonly timeoutMs: number;
}
function isAstroServerEntrypoint(value: unknown): value is AstroServerEntrypoint {
if (typeof value !== 'object') {
return false;
}
if (value == null) {
return false;
}
if (!('handler' in value)) {
return false;
}
return typeof value.handler === 'function';
}
class InvalidAstroServerListenPortError extends Error {
constructor() {
super('Astro server listen port must be an integer between 1 and 65535');
this.name = 'InvalidAstroServerListenPortError';
}
}
class AstroServerStartupAbandonedError extends Error {
public readonly phase: AstroServerStartupPhase;
constructor(phase: AstroServerStartupPhase) {
super(`${ASTRO_SERVER_NAME} ran out of pending work during ${phase} and would exit without serving`);
this.name = 'AstroServerStartupAbandonedError';
this.phase = phase;
}
}
class InvalidAstroServerHealthcheckTimeoutError extends Error {
constructor() {
super('Astro server healthcheck timeout must be a positive integer');
this.name = 'InvalidAstroServerHealthcheckTimeoutError';
}
}
class InvalidAstroServerPublicEndpointError extends Error {
constructor() {
super('Astro server public endpoint must not contain credentials, a query, or a fragment');
this.name = 'InvalidAstroServerPublicEndpointError';
}
}
function validateListener(listenHost: string, listenPort: number): void {
if (listenHost.length === 0) {
throw new Error('Astro server listen host must not be empty');
}
if (!Number.isInteger(listenPort)) {
throw new InvalidAstroServerListenPortError();
}
if (listenPort < 1) {
throw new InvalidAstroServerListenPortError();
}
if (listenPort > 65_535) {
throw new InvalidAstroServerListenPortError();
}
}
function resolvePublicEndpoint(publicEndpoint: string): ResolvedPublicEndpoint {
const endpoint = new URL(publicEndpoint);
if (!isCanonicalHTTPNetworkProtocol(endpoint.protocol)) {
throw new Error('Astro server public endpoint must use HTTP or HTTPS');
}
if (endpoint.username.length > 0) {
throw new InvalidAstroServerPublicEndpointError();
}
if (endpoint.password.length > 0) {
throw new InvalidAstroServerPublicEndpointError();
}
if (endpoint.search.length > 0) {
throw new InvalidAstroServerPublicEndpointError();
}
if (endpoint.hash.length > 0) {
throw new InvalidAstroServerPublicEndpointError();
}
const secureOrigin = endpoint.protocol === CanonicalNetworkProtocol.HTTPS;
if (endpoint.pathname === '/') {
return {basePath: '', secureOrigin};
}
return {basePath: endpoint.pathname.replace(/\/+$/u, ''), secureOrigin};
}
function healthcheckHost(listenHost: string): string {
if (listenHost === '0.0.0.0') {
return '127.0.0.1';
}
if (listenHost === '::') {
return '[::1]';
}
if (listenHost === '[::]') {
return '[::1]';
}
if (listenHost.includes(':') && !listenHost.startsWith('[')) {
return `[${listenHost}]`;
}
return listenHost;
}
function pathWithinBase(pathname: string, basePath: string): string | null {
if (basePath.length === 0) {
return pathname;
}
if (pathname === basePath) {
return '/';
}
if (pathname.startsWith(`${basePath}/`)) {
return pathname.slice(basePath.length);
}
return null;
}
function requestURL(request: IncomingMessage): URL | null {
const requestTarget = request.url;
if (requestTarget == null) {
return null;
}
if (!requestTarget.startsWith('/')) {
return null;
}
if (requestTarget.includes('#')) {
return null;
}
if (Buffer.byteLength(requestTarget) > ASTRO_SERVER_REQUEST_TARGET_MAX_BYTES) {
return null;
}
try {
return new URL(`${LOCAL_REQUEST_URL_BASE}${requestTarget}`);
} catch {
return null;
}
}
function resolveStaticRequestPath(
request: IncomingMessage,
requestURL: URL,
basePath: string,
): StaticRequestPath | null {
if (request.method !== HTTPMethod.GET) {
if (request.method !== HTTPMethod.HEAD) {
return null;
}
}
const staticPath = pathWithinBase(requestURL.pathname, basePath);
if (staticPath == null) {
return null;
}
return {
pathname: requestURL.pathname,
search: requestURL.search,
staticPath,
};
}
function normalizeServedPath(decodedPath: string): string {
if (decodedPath.length === 0) {
return decodedPath;
}
return path.normalize(`.${path.sep}${decodedPath}`);
}
async function resolveStaticFile(clientDirectory: string, staticPath: string): Promise<ResolvedStaticFile | null> {
let decodedPath: string;
try {
decodedPath = decodeURIComponent(staticPath);
} catch {
return null;
}
if (decodedPath.includes('\0')) {
return null;
}
const normalizedPath = normalizeServedPath(decodedPath);
if (PARENT_SEGMENT_PATTERN.test(normalizedPath)) {
return null;
}
const resolvedClientDirectory = path.resolve(clientDirectory);
const resolvedPath = path.resolve(resolvedClientDirectory, normalizedPath);
if (resolvedPath !== resolvedClientDirectory) {
if (!resolvedPath.startsWith(`${resolvedClientDirectory}${path.sep}`)) {
return null;
}
}
let isDirectory = false;
try {
isDirectory = (await lstat(resolvedPath)).isDirectory();
} catch (error) {
const code = getNodeErrorCode(error);
if (code !== NodeErrorCode.NOT_FOUND && code !== NodeErrorCode.NOT_DIRECTORY) throw error;
}
return {isDirectory, normalizedPath};
}
function respondToHealthcheck(
request: IncomingMessage,
requestURL: URL,
response: ServerResponse,
readiness: AstroServerReadiness | null,
): boolean {
if (requestURL.pathname !== HEALTH_PATH) {
return false;
}
if (isAstroServerReady(readiness)) {
return writeHealthcheckResponse(request, response, HTTPStatusCode.OK, HEALTH_RESPONSE_BODY);
}
return writeHealthcheckResponse(request, response, HTTPStatusCode.SERVICE_UNAVAILABLE, NOT_READY_RESPONSE_BODY);
}
function isAstroServerReady(readiness: AstroServerReadiness | null): boolean {
if (readiness == null) {
return true;
}
return readiness();
}
function writeHealthcheckResponse(
request: IncomingMessage,
response: ServerResponse,
status: HTTPStatusCode,
body: string,
): boolean {
response.writeHead(status, {
[HTTPHeader.CONTENT_TYPE]: MIMEType.JSON,
[HTTPHeader.CONTENT_LENGTH]: Buffer.byteLength(body).toString(),
[HTTPHeader.CACHE_CONTROL]: NO_STORE_CACHE_CONTROL,
});
if (request.method === HTTPMethod.HEAD) {
response.end();
return true;
}
response.end(body);
return true;
}
function writeTextResponse(
request: IncomingMessage,
response: ServerResponse,
status: HTTPStatusCode,
body: string,
): void {
response.writeHead(status, {
[HTTPHeader.CONTENT_TYPE]: TEXT_CONTENT_TYPE,
[HTTPHeader.CONTENT_LENGTH]: Buffer.byteLength(body).toString(),
[HTTPHeader.CACHE_CONTROL]: NO_STORE_CACHE_CONTROL,
});
if (request.method === HTTPMethod.HEAD) {
response.end();
return;
}
response.end(body);
}
function writeEmptyResponse(response: ServerResponse, status: HTTPStatusCode): void {
response.writeHead(status, {
[HTTPHeader.CONTENT_LENGTH]: '0',
[HTTPHeader.CACHE_CONTROL]: NO_STORE_CACHE_CONTROL,
});
response.end();
}
function rejectRequest(response: ServerResponse, status: HTTPStatusCode, allow: string | null): void {
response.shouldKeepAlive = false;
response.setHeader(HTTPHeader.CONTENT_LENGTH, '0');
response.setHeader(HTTPHeader.CACHE_CONTROL, NO_STORE_CACHE_CONTROL);
response.setHeader(HTTPHeader.CONNECTION, 'close');
if (allow != null) {
response.setHeader(HTTPHeader.ALLOW, allow);
}
response.writeHead(status);
response.end();
}
function requestDeclaresBody(request: IncomingMessage): boolean {
if (request.headers[HTTPHeader.TRANSFER_ENCODING.toLowerCase()] != null) {
return true;
}
if (request.headers[HTTPHeader.EXPECT.toLowerCase()] != null) {
return true;
}
const contentLength = request.headers[HTTPHeader.CONTENT_LENGTH.toLowerCase()];
if (contentLength == null) {
return false;
}
if (Array.isArray(contentLength)) {
return true;
}
return contentLength.trim() !== '0';
}
function sendFailure(request: IncomingMessage, response: ServerResponse, error: unknown): void {
if (response.headersSent) {
let resolvedError: Error;
if (error instanceof Error) {
resolvedError = error;
} else {
resolvedError = new Error(String(error));
}
response.destroy(resolvedError);
return;
}
writeTextResponse(request, response, HTTPStatusCode.INTERNAL_SERVER_ERROR, 'Internal server error');
}
interface AstroHandlerRequest {
readonly handler: AstroRequestHandler;
readonly request: IncomingMessage;
readonly requestLocals: AstroServerRequestLocals | null;
readonly response: ServerResponse;
}
interface StaticOrRenderRequest extends AstroHandlerRequest {
readonly clientDirectory: string;
readonly basePath: string;
readonly requestURL: URL;
}
async function runAstroHandler({handler, request, requestLocals, response}: AstroHandlerRequest): Promise<void> {
const next = (error: unknown): void => {
if (error != null) {
sendFailure(request, response, error);
return;
}
if (!response.writableEnded) {
writeEmptyResponse(response, HTTPStatusCode.NOT_FOUND);
}
};
try {
if (requestLocals == null) {
await handler(request, response, next, {});
return;
}
await handler(request, response, next, requestLocals());
} catch (error) {
sendFailure(request, response, error);
}
}
function shouldRedirectStaticDirectory(staticFile: ResolvedStaticFile, requestPath: StaticRequestPath): boolean {
return staticFile.isDirectory && requestPath.pathname !== '/' && !requestPath.pathname.endsWith('/');
}
async function serveStaticOrRender({
handler,
clientDirectory,
basePath,
request,
requestURL,
requestLocals,
response,
}: StaticOrRenderRequest): Promise<void> {
const requestPath = resolveStaticRequestPath(request, requestURL, basePath);
if (requestPath == null) {
await runAstroHandler({handler, request, requestLocals, response});
return;
}
const staticFile = await resolveStaticFile(clientDirectory, requestPath.staticPath);
if (staticFile == null) {
writeTextResponse(request, response, HTTPStatusCode.BAD_REQUEST, 'Bad request');
return;
}
if (shouldRedirectStaticDirectory(staticFile, requestPath)) {
response.writeHead(HTTPStatusCode.MOVED_PERMANENTLY, {
[HTTPHeader.LOCATION]: `${requestPath.pathname}/${requestPath.search}`,
[HTTPHeader.CONTENT_LENGTH]: '0',
[HTTPHeader.CACHE_CONTROL]: 'no-cache',
});
response.end();
return;
}
let encodedPath = requestPath.staticPath;
let normalizedPath = staticFile.normalizedPath;
if (staticFile.isDirectory) {
if (!encodedPath.endsWith('/')) {
encodedPath = `${encodedPath}/index.html`;
normalizedPath = normalizeServedPath(`${normalizedPath}/index.html`);
}
}
let dotfiles: StaticDotfilePolicy = StaticDotfilePolicy.DENY;
if (normalizedPath.startsWith(WELL_KNOWN_PATH_PREFIX)) {
dotfiles = StaticDotfilePolicy.ALLOW;
}
const stream = send(request, encodedPath, {
dotfiles,
extensions: ['html'],
index: ['index.html'],
root: clientDirectory,
});
let fileOpened = false;
stream.on('file', () => {
fileOpened = true;
if (normalizedPath.startsWith(IMMUTABLE_ASSET_PATH_PREFIX)) {
response.setHeader(HTTPHeader.CACHE_CONTROL, IMMUTABLE_ASSET_CACHE_CONTROL);
}
});
stream.on('error', (error) => {
if (!fileOpened) {
runAstroHandler({handler, request, requestLocals, response}).catch((handlerError: unknown) => {
sendFailure(request, response, handlerError);
});
return;
}
sendFailure(request, response, error);
});
stream.pipe(response);
}
async function loadServerEntrypoint(entrypointURL: URL): Promise<AstroServerEntrypoint> {
const entrypoint: unknown = await import(entrypointURL.href);
if (!isAstroServerEntrypoint(entrypoint)) {
throw new Error(`Astro server entrypoint does not export a handler: ${entrypointURL.href}`);
}
return entrypoint;
}
function closeServerListener(server: Server): Promise<void> {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error == null) {
resolve();
return;
}
reject(error);
});
server.closeIdleConnections();
});
}
export async function runAstroServer(options: AstroServerOptions): Promise<void> {
validateListener(options.listenHost, options.listenPort);
const {basePath, secureOrigin} = resolvePublicEndpoint(options.publicEndpoint);
const securityHeaders = siteSecurityHeaders(secureOrigin);
let startupPhase: AstroServerStartupPhase | null = AstroServerStartupPhase.ENTRYPOINT;
const requireStartupProgress = (): void => {
const phase = startupPhase;
if (phase == null) {
return;
}
throw new AstroServerStartupAbandonedError(phase);
};
process.on('beforeExit', requireStartupProgress);
try {
const entrypoint = await loadServerEntrypoint(options.entrypoint);
if (options.initialize != null) {
startupPhase = AstroServerStartupPhase.INITIALIZE;
await options.initialize();
}
const clientDirectory = fileURLToPath(new URL('../client/', options.entrypoint));
let activeRequests = 0;
const handleRequest = (request: IncomingMessage, response: ServerResponse): void => {
for (const header of securityHeaders) {
response.setHeader(header.name, header.value);
}
const parsedRequestURL = requestURL(request);
if (parsedRequestURL == null) {
rejectRequest(response, HTTPStatusCode.BAD_REQUEST, null);
return;
}
if (request.method !== HTTPMethod.GET && request.method !== HTTPMethod.HEAD) {
rejectRequest(response, HTTPStatusCode.METHOD_NOT_ALLOWED, ALLOWED_SITE_METHODS);
return;
}
if (requestDeclaresBody(request)) {
rejectRequest(response, HTTPStatusCode.PAYLOAD_TOO_LARGE, null);
return;
}
if (respondToHealthcheck(request, parsedRequestURL, response, options.readiness)) {
return;
}
if (activeRequests >= ASTRO_SERVER_MAX_ACTIVE_REQUESTS) {
rejectRequest(response, HTTPStatusCode.SERVICE_UNAVAILABLE, null);
return;
}
activeRequests += 1;
let retained = true;
const release = (): void => {
if (!retained) {
return;
}
retained = false;
activeRequests -= 1;
};
response.once('finish', release);
response.once('close', release);
serveStaticOrRender({
handler: entrypoint.handler,
clientDirectory,
basePath,
request,
requestURL: parsedRequestURL,
requestLocals: options.requestLocals,
response,
}).catch((error: unknown) => {
sendFailure(request, response, error);
});
};
const server = createServer({maxHeaderSize: ASTRO_SERVER_MAX_HEADER_BYTES}, handleRequest);
server.on('checkContinue', handleRequest);
server.requestTimeout = ASTRO_SERVER_REQUEST_TIMEOUT_MS;
server.headersTimeout = ASTRO_SERVER_HEADERS_TIMEOUT_MS;
server.keepAliveTimeout = ASTRO_SERVER_KEEP_ALIVE_TIMEOUT_MS;
server.timeout = ASTRO_SERVER_SOCKET_TIMEOUT_MS;
server.maxHeadersCount = ASTRO_SERVER_MAX_HEADERS;
server.maxRequestsPerSocket = ASTRO_SERVER_MAX_REQUESTS_PER_SOCKET;
server.maxConnections = ASTRO_SERVER_MAX_CONNECTIONS;
const done = new Promise<void>((resolve, reject) => {
server.once('close', resolve);
server.once('error', reject);
});
new AstroServerTermination({
closeListener: () => closeServerListener(server),
serverName: ASTRO_SERVER_NAME,
shutdown: options.shutdown,
timeoutMs: ASTRO_SERVER_SHUTDOWN_TIMEOUT_MS,
}).install();
startupPhase = AstroServerStartupPhase.LISTEN;
server.once('listening', () => {
startupPhase = null;
});
server.listen(options.listenPort, options.listenHost);
await done;
} finally {
process.off('beforeExit', requireStartupProgress);
}
}
export async function checkAstroServerHealth(options: AstroServerHealthcheckOptions): Promise<void> {
validateListener(options.listenHost, options.listenPort);
if (!Number.isInteger(options.timeoutMs)) {
throw new InvalidAstroServerHealthcheckTimeoutError();
}
if (options.timeoutMs < 1) {
throw new InvalidAstroServerHealthcheckTimeoutError();
}
const endpoint = [
CanonicalNetworkProtocol.HTTP,
'//',
healthcheckHost(options.listenHost),
':',
options.listenPort.toString(),
HEALTH_PATH,
].join('');
const response = await fetch(endpoint, {
cache: 'no-store',
credentials: 'omit',
headers: {[HTTPHeader.USER_AGENT]: OUTBOUND_USER_AGENT},
redirect: 'error',
referrerPolicy: 'no-referrer',
signal: AbortSignal.timeout(options.timeoutMs),
});
if (response.body != null) {
await response.body.cancel();
}
if (!response.ok) {
throw new Error(`Astro server healthcheck failed with status ${response.status.toString()}`);
}
}
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export type AstroServerShutdown = () => Promise<void>;
interface AstroServerTerminationDependencies {
readonly closeListener: () => Promise<void>;
readonly serverName: string;
readonly shutdown: AstroServerShutdown | null;
readonly timeoutMs: number;
}
export class AstroServerTermination {
private installed = false;
private terminationPromise: Promise<void> | null = null;
public constructor(private readonly dependencies: AstroServerTerminationDependencies) {}
public install(): void {
if (this.installed) {
throw new Error(`${this.dependencies.serverName} termination handlers are already installed`);
}
this.installed = true;
process.on('SIGINT', this.handleSIGINT);
process.on('SIGTERM', this.handleSIGTERM);
}
private readonly handleSIGINT = (): void => {
this.startTermination('SIGINT');
};
private readonly handleSIGTERM = (): void => {
this.startTermination('SIGTERM');
};
private startTermination(signal: NodeJS.Signals): void {
if (this.terminationPromise != null) {
return;
}
this.terminationPromise = this.performTermination(signal);
}
private async performTermination(signal: NodeJS.Signals): Promise<void> {
const watchdog = setTimeout(() => {
console.error(
`Forcing ${this.dependencies.serverName} termination after cleanup timeout`,
`${signal} ${this.dependencies.timeoutMs.toString()}ms`,
);
process.exit(1);
}, this.dependencies.timeoutMs);
const failures = await this.releaseResources();
clearTimeout(watchdog);
if (failures.length === 0) {
process.exit(0);
}
const message = `${this.dependencies.serverName} shutdown failed`;
console.error(message, new AggregateError(failures, message));
process.exit(1);
}
private async releaseResources(): Promise<Array<unknown>> {
const failures: Array<unknown> = [];
try {
await this.dependencies.closeListener();
} catch (cleanupFailure) {
failures.push(cleanupFailure);
}
const shutdown = this.dependencies.shutdown;
if (shutdown == null) {
return failures;
}
try {
await shutdown();
} catch (cleanupFailure) {
failures.push(cleanupFailure);
}
return failures;
}
}
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {buildUrl} from '@fluxer/config/src/EndpointDerivation';
const DEFAULT_BASE_DOMAIN = 'localhost';
const DEFAULT_PUBLIC_SCHEME = 'http';
const DEFAULT_LISTEN_HOST = '0.0.0.0';
const DEFAULT_LISTEN_PORT = 8080;
function readString(name: string, fallback: string): string {
const value = process.env[name];
if (value == null) {
return fallback;
}
if (value.length === 0) {
return fallback;
}
return value;
}
function readPort(name: string): number | undefined {
const value = process.env[name];
if (value == null) {
return undefined;
}
if (value.length === 0) {
return undefined;
}
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed)) {
return undefined;
}
return parsed;
}
function readScheme(name: string): 'http' | 'https' {
if (readString(name, DEFAULT_PUBLIC_SCHEME) === 'https') {
return 'https';
}
return 'http';
}
export function docsBaseDomain(): string {
return readString('FLUXER_BASE_DOMAIN', DEFAULT_BASE_DOMAIN);
}
export function docsPublicEndpoint(): string {
const override = process.env.FLUXER_DOCS_ENDPOINT;
if (override != null && override.length > 0) {
return override;
}
return buildUrl(readScheme('FLUXER_PUBLIC_SCHEME'), docsBaseDomain(), readPort('FLUXER_PUBLIC_PORT'));
}
export function docsListenHost(): string {
return readString('FLUXER_DOCS_LISTEN_HOST', DEFAULT_LISTEN_HOST);
}
export function docsListenPort(): number {
const parsed = readPort('FLUXER_DOCS_LISTEN_PORT');
if (parsed == null) {
return DEFAULT_LISTEN_PORT;
}
if (parsed < 1) {
return DEFAULT_LISTEN_PORT;
}
if (parsed > 65_535) {
return DEFAULT_LISTEN_PORT;
}
return parsed;
}
export function docsSearchIndexing(): boolean {
return readString('FLUXER_DOCS_SEARCH_INDEXING', 'false') === 'true';
}
+81
View File
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export const HTTPMethod = Object.freeze({
GET: 'GET',
HEAD: 'HEAD',
} as const);
export type HTTPMethod = (typeof HTTPMethod)[keyof typeof HTTPMethod];
export const HTTPStatusCode = Object.freeze({
OK: 200,
MOVED_PERMANENTLY: 301,
BAD_REQUEST: 400,
NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
PAYLOAD_TOO_LARGE: 413,
INTERNAL_SERVER_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
} as const);
export type HTTPStatusCode = (typeof HTTPStatusCode)[keyof typeof HTTPStatusCode];
export const MIMEType = Object.freeze({
JSON: 'application/json',
PLAIN: 'text/plain',
} as const);
export const HTTPHeader = Object.freeze({
ALLOW: 'Allow',
CACHE_CONTROL: 'Cache-Control',
CONNECTION: 'Connection',
CONTENT_LENGTH: 'Content-Length',
CONTENT_TYPE: 'Content-Type',
EXPECT: 'Expect',
LOCATION: 'Location',
PERMISSIONS_POLICY: 'Permissions-Policy',
REFERRER_POLICY: 'Referrer-Policy',
STRICT_TRANSPORT_SECURITY: 'Strict-Transport-Security',
TRANSFER_ENCODING: 'Transfer-Encoding',
USER_AGENT: 'User-Agent',
X_CONTENT_TYPE_OPTIONS: 'X-Content-Type-Options',
X_FRAME_OPTIONS: 'X-Frame-Options',
} as const);
export const OUTBOUND_USER_AGENT = 'Fluxer (https://fluxer.app)';
export const CanonicalNetworkProtocol = Object.freeze({
HTTP: 'http:',
HTTPS: 'https:',
} as const);
export type CanonicalNetworkProtocol = (typeof CanonicalNetworkProtocol)[keyof typeof CanonicalNetworkProtocol];
export function isCanonicalHTTPNetworkProtocol(protocol: string): boolean {
if (protocol === CanonicalNetworkProtocol.HTTP) {
return true;
}
return protocol === CanonicalNetworkProtocol.HTTPS;
}
export const NodeErrorCode = Object.freeze({
NOT_FOUND: 'ENOENT',
NOT_DIRECTORY: 'ENOTDIR',
} as const);
export function getNodeErrorCode(error: unknown): string | null {
if (typeof error !== 'object') {
return null;
}
if (error == null) {
return null;
}
if (!('code' in error)) {
return null;
}
const code = (error as {code: unknown}).code;
if (typeof code !== 'string') {
return null;
}
return code;
}
+34
View File
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {HTTPHeader} from './HTTPConstants';
const PERMISSIONS_POLICY =
'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()';
const REFERRER_POLICY = 'strict-origin-when-cross-origin';
const CONTENT_TYPE_OPTIONS = 'nosniff';
const FRAME_OPTIONS = 'DENY';
const STRICT_TRANSPORT_SECURITY = 'max-age=31536000; includeSubDomains; preload';
interface SiteSecurityHeader {
readonly name: string;
readonly value: string;
}
const INSECURE_ORIGIN_HEADERS: ReadonlyArray<SiteSecurityHeader> = Object.freeze([
Object.freeze({name: HTTPHeader.PERMISSIONS_POLICY, value: PERMISSIONS_POLICY}),
Object.freeze({name: HTTPHeader.REFERRER_POLICY, value: REFERRER_POLICY}),
Object.freeze({name: HTTPHeader.X_CONTENT_TYPE_OPTIONS, value: CONTENT_TYPE_OPTIONS}),
Object.freeze({name: HTTPHeader.X_FRAME_OPTIONS, value: FRAME_OPTIONS}),
]);
const SECURE_ORIGIN_HEADERS: ReadonlyArray<SiteSecurityHeader> = Object.freeze([
...INSECURE_ORIGIN_HEADERS,
Object.freeze({name: HTTPHeader.STRICT_TRANSPORT_SECURITY, value: STRICT_TRANSPORT_SECURITY}),
]);
export function siteSecurityHeaders(secureOrigin: boolean): ReadonlyArray<SiteSecurityHeader> {
if (secureOrigin) {
return SECURE_ORIGIN_HEADERS;
}
return INSECURE_ORIGIN_HEADERS;
}
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {CanonicalNetworkProtocol} from './HTTPConstants';
const ViteDevWebSocketProtocol = Object.freeze({
WS: 'ws',
WSS: 'wss',
} as const);
type ViteDevWebSocketProtocol = (typeof ViteDevWebSocketProtocol)[keyof typeof ViteDevWebSocketProtocol];
interface ViteDevWebSocketOptions {
readonly endpoint: URL;
readonly path: string;
}
interface ViteDevWebSocketConfig {
readonly protocol: ViteDevWebSocketProtocol;
readonly host: string;
readonly clientPort: number;
readonly path: string;
}
function resolveProtocol(endpoint: URL): ViteDevWebSocketProtocol {
if (endpoint.protocol === CanonicalNetworkProtocol.HTTPS) {
return ViteDevWebSocketProtocol.WSS;
}
return ViteDevWebSocketProtocol.WS;
}
function resolveClientPort(endpoint: URL): number {
if (endpoint.port.length > 0) {
return Number.parseInt(endpoint.port, 10);
}
if (endpoint.protocol === CanonicalNetworkProtocol.HTTPS) {
return 443;
}
return 80;
}
export function resolveViteDevWebSocket({endpoint, path}: ViteDevWebSocketOptions): ViteDevWebSocketConfig {
return {
protocol: resolveProtocol(endpoint),
host: endpoint.hostname,
clientPort: resolveClientPort(endpoint),
path,
};
}