Merge pull request #3003 from fosrl/dev

1.18.2-s.3
This commit is contained in:
Owen Schwartz
2026-05-05 10:54:48 -07:00
committed by GitHub
14 changed files with 141 additions and 32 deletions

View File

@@ -1,5 +1,6 @@
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3"; import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import type BetterSqlite3 from "better-sqlite3";
import * as schema from "./schema/schema"; import * as schema from "./schema/schema";
import path from "path"; import path from "path";
import fs from "fs"; import fs from "fs";
@@ -11,8 +12,69 @@ export const exists = checkFileExists(location);
bootstrapVolume(); bootstrapVolume();
/**
* Wraps better-sqlite3 Statement to call `finalize()` immediately after
* execution, freeing native sqlite3_stmt memory deterministically instead
* of waiting for GC. Fixes steady off-heap growth under load (#2120).
* WARNING: Finalizes after first execution — incompatible with drizzle's
* reusable .prepare() builders. No such usage exists in this codebase.
*/
function autoFinalizeStatement(
stmt: BetterSqlite3.Statement
): BetterSqlite3.Statement {
const wrapExec = <T extends (...args: any[]) => any>(fn: T): T => {
return function (this: any, ...args: any[]) {
try {
return fn.apply(this, args);
} finally {
try {
// finalize() exists on the native Statement at runtime but
// is missing from @types/better-sqlite3.
(stmt as any).finalize();
} catch {
// Already finalized — harmless
}
}
} as unknown as T;
};
stmt.run = wrapExec(stmt.run);
stmt.get = wrapExec(stmt.get);
stmt.all = wrapExec(stmt.all);
return stmt;
}
function createDb() { function createDb() {
const sqlite = new Database(location); const sqlite = new Database(location);
if (process.env.ENABLE_SQLITE_WAL_MODE == "true") {
// Enable WAL mode — allows concurrent readers + single writer, preventing
// contention across subsystems (verifySession, Traefik, audit, ping).
sqlite.pragma("journal_mode = WAL");
// NORMAL sync mode: safe with WAL, reduces write lock hold time.
sqlite.pragma("synchronous = NORMAL");
}
// Wait up to 5s on SQLITE_BUSY instead of failing — prevents audit log
// retry loops that accumulate memory.
sqlite.pragma("busy_timeout = 5000");
// 64 MB page cache (default 2 MB) — reduces I/O round-trips on large
// TraefikConfigManager JOINs that block the event loop.
sqlite.pragma("cache_size = -65536");
// 256 MB memory-mapped I/O — OS serves reads from page cache directly,
// reducing event-loop blocking.
sqlite.pragma("mmap_size = 268435456");
// Wrap prepare() so every drizzle-orm statement is auto-finalized after
// first use, preventing sqlite3_stmt accumulation between GC cycles.
const originalPrepare = sqlite.prepare.bind(sqlite);
(sqlite as any).prepare = function autoFinalizePrepare(source: string) {
return autoFinalizeStatement(originalPrepare(source));
};
return DrizzleSqlite(sqlite, { return DrizzleSqlite(sqlite, {
schema schema
}); });
@@ -23,7 +85,7 @@ export default db;
export const primaryDb = db; export const primaryDb = db;
export type Transaction = Parameters< export type Transaction = Parameters<
Parameters<(typeof db)["transaction"]>[0] Parameters<(typeof db)["transaction"]>[0]
>[0]; >[0];
export const DB_TYPE: "pg" | "sqlite" = "sqlite"; export const DB_TYPE: "pg" | "sqlite" = "sqlite";
function checkFileExists(filePath: string): boolean { function checkFileExists(filePath: string): boolean {

View File

@@ -22,7 +22,7 @@ import {
Olm, Olm,
olms, olms,
RemoteExitNode, RemoteExitNode,
remoteExitNodes, remoteExitNodes
} from "@server/db"; } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "@server/db"; import { db } from "@server/db";
@@ -194,8 +194,6 @@ const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
// Config version tracking map (local to this node, resets on server restart) // Config version tracking map (local to this node, resets on server restart)
const clientConfigVersions: Map<string, number> = new Map(); const clientConfigVersions: Map<string, number> = new Map();
// Recovery tracking // Recovery tracking
let isRedisRecoveryInProgress = false; let isRedisRecoveryInProgress = false;
@@ -406,6 +404,9 @@ const removeClient = async (
const updatedClients = existingClients.filter((client) => client !== ws); const updatedClients = existingClients.filter((client) => client !== ws);
if (updatedClients.length === 0) { if (updatedClients.length === 0) {
connectedClients.delete(mapKey); connectedClients.delete(mapKey);
// Remove clientId from clientConfigVersions on disconnect — prevents
// unbounded memory growth from stale entries.
clientConfigVersions.delete(clientId);
if (redisManager.isRedisEnabled()) { if (redisManager.isRedisEnabled()) {
try { try {
@@ -1097,6 +1098,11 @@ const disconnectClient = async (clientId: string): Promise<boolean> => {
} }
}); });
// Eagerly remove client — close event may not fire if socket is already
// CLOSING, leaving zombie entries.
connectedClients.delete(mapKey);
clientConfigVersions.delete(clientId);
return true; return true;
}; };

View File

@@ -3,7 +3,15 @@ import zlib from "zlib";
import { Server as HttpServer } from "http"; import { Server as HttpServer } from "http";
import { WebSocket, WebSocketServer } from "ws"; import { WebSocket, WebSocketServer } from "ws";
import { Socket } from "net"; import { Socket } from "net";
import { Newt, newts, NewtSession, olms, Olm, OlmSession, sites } from "@server/db"; import {
Newt,
newts,
NewtSession,
olms,
Olm,
OlmSession,
sites
} from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { db } from "@server/db"; import { db } from "@server/db";
import { recordPing } from "@server/routers/newt/pingAccumulator"; import { recordPing } from "@server/routers/newt/pingAccumulator";
@@ -80,6 +88,9 @@ const removeClient = async (
const updatedClients = existingClients.filter((client) => client !== ws); const updatedClients = existingClients.filter((client) => client !== ws);
if (updatedClients.length === 0) { if (updatedClients.length === 0) {
connectedClients.delete(mapKey); connectedClients.delete(mapKey);
// Remove clientId from clientConfigVersions — prevents unbounded growth
// from stale entries.
clientConfigVersions.delete(clientId);
logger.info( logger.info(
`All connections removed for ${clientType.toUpperCase()} ID: ${clientId}` `All connections removed for ${clientType.toUpperCase()} ID: ${clientId}`
@@ -218,9 +229,13 @@ const hasActiveConnections = async (clientId: string): Promise<boolean> => {
}; };
// Get the current config version for a client // Get the current config version for a client
const getClientConfigVersion = async (clientId: string): Promise<number | undefined> => { const getClientConfigVersion = async (
clientId: string
): Promise<number | undefined> => {
const version = clientConfigVersions.get(clientId); const version = clientConfigVersions.get(clientId);
logger.debug(`getClientConfigVersion called for clientId: ${clientId}, returning: ${version} (type: ${typeof version})`); logger.debug(
`getClientConfigVersion called for clientId: ${clientId}, returning: ${version} (type: ${typeof version})`
);
return version; return version;
}; };
@@ -507,6 +522,11 @@ const disconnectClient = async (clientId: string): Promise<boolean> => {
} }
}); });
// Eagerly remove client — close event may not fire if socket already
// CLOSING, leaving zombie entries.
connectedClients.delete(mapKey);
clientConfigVersions.delete(clientId);
return true; return true;
}; };

View File

@@ -31,8 +31,9 @@ export function CertificateStatusContent({
const t = useTranslations(); const t = useTranslations();
const labelClass = const labelClass =
"inline-flex shrink-0 items-center self-center text-sm font-medium leading-none"; "inline-flex shrink-0 items-center self-center text-sm font-medium leading-normal";
const valueClass = "inline-flex items-center gap-2 text-sm leading-none"; const valueClass =
"inline-flex items-center gap-2 text-sm leading-normal";
const handleRefresh = async () => { const handleRefresh = async () => {
await refreshCert(); await refreshCert();
@@ -133,14 +134,14 @@ export function CertificateStatusContent({
{isPending && !disableRestartButton ? ( {isPending && !disableRestartButton ? (
<Button <Button
variant="ghost" variant="ghost"
className="h-auto min-h-0 shrink-0 p-0 text-sm font-normal leading-none inline-flex items-center self-center" className="h-auto min-h-0 shrink-0 p-0 text-sm font-normal leading-normal inline-flex items-center self-center"
onClick={handleRefresh} onClick={handleRefresh}
disabled={refreshing} disabled={refreshing}
title={t("restartCertificate", { title={t("restartCertificate", {
defaultValue: "Restart Certificate" defaultValue: "Restart Certificate"
})} })}
> >
<span className="inline-flex items-center gap-2 leading-none"> <span className="inline-flex items-center gap-2 leading-normal">
<FileBadge <FileBadge
className={`h-4 w-4 shrink-0 ${getStatusColor(cert.status)}`} className={`h-4 w-4 shrink-0 ${getStatusColor(cert.status)}`}
aria-hidden aria-hidden
@@ -148,7 +149,7 @@ export function CertificateStatusContent({
{cert.status.charAt(0).toUpperCase() + {cert.status.charAt(0).toUpperCase() +
cert.status.slice(1)} cert.status.slice(1)}
<RotateCw <RotateCw
className={`h-3 w-3 shrink-0 ${refreshing ? "animate-spin" : ""}`} className={`h-4 w-4 shrink-0 ${refreshing ? "animate-spin" : ""}`}
/> />
</span> </span>
</Button> </Button>
@@ -164,7 +165,7 @@ export function CertificateStatusContent({
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
className="inline-flex h-auto min-h-0 w-3 shrink-0 items-center justify-center self-center p-0" className="inline-flex h-4 w-4 min-h-0 shrink-0 items-center justify-center self-center p-0"
onClick={handleRefresh} onClick={handleRefresh}
disabled={refreshing} disabled={refreshing}
title={t("restartCertificate", { title={t("restartCertificate", {
@@ -172,7 +173,7 @@ export function CertificateStatusContent({
})} })}
> >
<RotateCw <RotateCw
className={`h-3 w-3 shrink-0 ${refreshing ? "animate-spin" : ""}`} className={`h-4 w-4 shrink-0 ${refreshing ? "animate-spin" : ""}`}
/> />
</Button> </Button>
) : null} ) : null}

View File

@@ -33,7 +33,7 @@ const CopyToClipboard = ({
<div className="flex items-center space-x-2 min-w-0 max-w-full"> <div className="flex items-center space-x-2 min-w-0 max-w-full">
<button <button
type="button" type="button"
className="h-6 w-6 p-0 flex items-center justify-center cursor-pointer flex-shrink-0" className="h-4 w-4 p-0 flex items-center justify-center cursor-pointer flex-shrink-0"
onClick={handleCopy} onClick={handleCopy}
> >
{!copied ? ( {!copied ? (

View File

@@ -104,7 +104,7 @@ export default function IdpLoginButtons({
</Alert> </Alert>
)} )}
<div className="space-y-2"> <div className="space-y-4">
{params.get("gotoapp") ? ( {params.get("gotoapp") ? (
<> <>
<Button <Button

View File

@@ -19,7 +19,7 @@ export function InfoSections({
return ( return (
<div <div
className={cn( className={cn(
"grid grid-cols-2 md:grid-cols-(--columns) md:space-x-16 gap-4 md:items-start", "grid w-full min-w-0 grid-cols-2 md:grid-cols-(--columns) md:space-x-16 gap-4 md:items-start",
columnSizing === "content" && columnSizing === "content" &&
"md:justify-items-start md:justify-start" "md:justify-items-start md:justify-start"
)} )}
@@ -41,7 +41,11 @@ export function InfoSection({
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
}) { }) {
return <div className={cn("space-y-1", className)}>{children}</div>; return (
<div className={cn("min-w-0 w-full max-w-full space-y-1", className)}>
{children}
</div>
);
} }
export function InfoSectionTitle({ export function InfoSectionTitle({
@@ -51,7 +55,11 @@ export function InfoSectionTitle({
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
}) { }) {
return <div className={cn("font-semibold", className)}>{children}</div>; return (
<div className={cn("min-w-0 truncate font-semibold", className)}>
{children}
</div>
);
} }
export function InfoSectionContent({ export function InfoSectionContent({
@@ -62,8 +70,13 @@ export function InfoSectionContent({
className?: string; className?: string;
}) { }) {
return ( return (
<div className={cn("min-w-0 overflow-hidden", className)}> <div
<div className="w-full truncate [&>div.flex]:min-w-0 [&>div.flex]:!whitespace-normal [&>div.flex>span]:truncate [&>div.flex>a]:truncate"> className={cn(
"w-full min-w-0 max-w-full overflow-hidden",
className
)}
>
<div className="w-full min-w-0 max-w-full truncate [&>div.flex]:min-w-0 [&>div.flex]:!whitespace-normal [&>div.flex>span]:truncate [&>div.flex>a]:truncate">
{children} {children}
</div> </div>
</div> </div>

View File

@@ -368,7 +368,7 @@ export default function LoginForm({
{hasIdp && ( {hasIdp && (
<> <>
<div className="relative my-4"> <div className="relative">
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">
<Separator /> <Separator />
</div> </div>

View File

@@ -145,7 +145,7 @@ export default function MfaInputForm({
</Alert> </Alert>
)} )}
<div className="space-y-2"> <div className="space-y-4">
<Button <Button
type="submit" type="submit"
form={formId} form={formId}

View File

@@ -528,7 +528,7 @@ export default function ResetPasswordForm({
)} )}
{state === "request" && ( {state === "request" && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-4">
{env.email.emailEnabled && ( {env.email.emailEnabled && (
<Button <Button
type="submit" type="submit"

View File

@@ -40,7 +40,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<InfoSection> <InfoSection>
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle> <InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{resource.niceId} <span className="inline-flex items-center">
{resource.niceId}
</span>
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
{resource.http ? ( {resource.http ? (
@@ -49,7 +51,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<InfoSectionTitle>URL</InfoSectionTitle> <InfoSectionTitle>URL</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{resource.wildcard ? ( {resource.wildcard ? (
<span>{fullUrl}</span> <span className="inline-flex items-center">
{fullUrl}
</span>
) : ( ) : (
<CopyToClipboard <CopyToClipboard
text={fullUrl} text={fullUrl}
@@ -68,7 +72,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
authInfo.sso || authInfo.sso ||
authInfo.whitelist || authInfo.whitelist ||
authInfo.headerAuth ? ( authInfo.headerAuth ? (
<div className="flex items-start space-x-2"> <div className="flex items-center space-x-2">
<ShieldCheck className="w-4 h-4 flex-shrink-0 text-green-500" /> <ShieldCheck className="w-4 h-4 flex-shrink-0 text-green-500" />
<span>{t("protected")}</span> <span>{t("protected")}</span>
</div> </div>
@@ -106,7 +110,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
{t("protocol")} {t("protocol")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{resource.protocol.toUpperCase()} <span className="inline-flex items-center">
{resource.protocol.toUpperCase()}
</span>
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>

View File

@@ -284,7 +284,7 @@ export default function SmartLoginForm({
{orgSignIn && ( {orgSignIn && (
<> <>
<div className="relative my-4"> <div className="relative">
<div className="absolute inset-0 flex items-center"> <div className="absolute inset-0 flex items-center">
<Separator /> <Separator />
</div> </div>

View File

@@ -207,7 +207,7 @@ export default function SmartLoginOrgSelector({
/> />
{hasInternalAccount && ( {hasInternalAccount && (
<div className="mt-3"> <div className="mt-4">
<Button <Button
type="button" type="button"
className="w-full" className="w-full"
@@ -237,7 +237,7 @@ export default function SmartLoginOrgSelector({
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-4">
{params.get("gotoapp") ? ( {params.get("gotoapp") ? (
<Button <Button
type="button" type="button"

View File

@@ -17,6 +17,7 @@ import { Loader2, CheckCircle2, AlertCircle } from "lucide-react";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { validateOidcUrlCallbackProxy } from "@app/actions/server"; import { validateOidcUrlCallbackProxy } from "@app/actions/server";
import { build } from "@server/build";
type ValidateOidcTokenParams = { type ValidateOidcTokenParams = {
orgId: string; orgId: string;
@@ -96,7 +97,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
stateCookie: props.stateCookie stateCookie: props.stateCookie
}); });
if (isLicenseViolation()) { if (build === "enterprise" && isLicenseViolation()) {
await new Promise((resolve) => setTimeout(resolve, 5000)); await new Promise((resolve) => setTimeout(resolve, 5000));
} }