Merge branch 'dev' into resource-policies

This commit is contained in:
Owen
2026-05-04 17:32:24 -07:00
15 changed files with 157 additions and 50 deletions

View File

@@ -1,5 +1,6 @@
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import type BetterSqlite3 from "better-sqlite3";
import * as schema from "./schema/schema";
import path from "path";
import fs from "fs";
@@ -11,8 +12,68 @@ export const exists = checkFileExists(location);
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() {
const sqlite = new Database(location);
// Enable WAL mode — allows concurrent readers + single writer, preventing
// contention across subsystems (verifySession, Traefik, audit, ping).
sqlite.pragma("journal_mode = WAL");
// Wait up to 5s on SQLITE_BUSY instead of failing — prevents audit log
// retry loops that accumulate memory.
sqlite.pragma("busy_timeout = 5000");
// NORMAL sync mode: safe with WAL, reduces write lock hold time.
sqlite.pragma("synchronous = NORMAL");
// 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, {
schema
});
@@ -23,7 +84,7 @@ export default db;
export const primaryDb = db;
export type Transaction = Parameters<
Parameters<(typeof db)["transaction"]>[0]
>[0];
>[0];
export const DB_TYPE: "pg" | "sqlite" = "sqlite";
function checkFileExists(filePath: string): boolean {

View File

@@ -22,7 +22,7 @@ import {
Olm,
olms,
RemoteExitNode,
remoteExitNodes,
remoteExitNodes
} from "@server/db";
import { eq } from "drizzle-orm";
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)
const clientConfigVersions: Map<string, number> = new Map();
// Recovery tracking
let isRedisRecoveryInProgress = false;
@@ -406,6 +404,9 @@ const removeClient = async (
const updatedClients = existingClients.filter((client) => client !== ws);
if (updatedClients.length === 0) {
connectedClients.delete(mapKey);
// Remove clientId from clientConfigVersions on disconnect — prevents
// unbounded memory growth from stale entries.
clientConfigVersions.delete(clientId);
if (redisManager.isRedisEnabled()) {
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;
};

View File

@@ -671,7 +671,8 @@ export async function verifyResourceSession(
resourceData.org
);
localCache.set(userAccessCacheKey, allowedUserData, 5);
// this is query intensive so let it cache a little longer
localCache.set(userAccessCacheKey, allowedUserData, 12);
}
if (
@@ -1003,11 +1004,7 @@ async function checkRules(
isIpInCidr(clientIp, rule.value)
) {
return rule.action as any;
} else if (
clientIp &&
rule.match == "IP" &&
clientIp == rule.value
) {
} else if (clientIp && rule.match == "IP" && clientIp == rule.value) {
return rule.action as any;
} else if (
path &&
@@ -1015,10 +1012,7 @@ async function checkRules(
isPathAllowed(rule.value, path)
) {
return rule.action as any;
} else if (
clientIp &&
rule.match == "COUNTRY"
) {
} else if (clientIp && rule.match == "COUNTRY") {
// COUNTRY=ALL should not affect local/private/CGNAT addresses.
if (
rule.value.toUpperCase() === "ALL" &&
@@ -1030,10 +1024,7 @@ async function checkRules(
if (await isIpInGeoIP(ipCC, rule.value)) {
return rule.action as any;
}
} else if (
clientIp &&
rule.match == "ASN"
) {
} else if (clientIp && rule.match == "ASN") {
// ASN=ALL/AS0 should not affect local/private/CGNAT addresses.
if (
(rule.value.toUpperCase() === "ALL" ||
@@ -1272,11 +1263,15 @@ export async function isIpInRegion(
if (region.id === checkRegionCode) {
for (const subregion of region.includes) {
if (subregion.countries.includes(upperCode)) {
logger.debug(`Country ${upperCode} is in region ${region.id} (${region.name})`);
logger.debug(
`Country ${upperCode} is in region ${region.id} (${region.name})`
);
return true;
}
}
logger.debug(`Country ${upperCode} is not in region ${region.id} (${region.name})`);
logger.debug(
`Country ${upperCode} is not in region ${region.id} (${region.name})`
);
return false;
}
@@ -1284,10 +1279,14 @@ export async function isIpInRegion(
for (const subregion of region.includes) {
if (subregion.id === checkRegionCode) {
if (subregion.countries.includes(upperCode)) {
logger.debug(`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`);
logger.debug(
`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`
);
return true;
}
logger.debug(`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`);
logger.debug(
`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`
);
return false;
}
}

View File

@@ -3,7 +3,15 @@ import zlib from "zlib";
import { Server as HttpServer } from "http";
import { WebSocket, WebSocketServer } from "ws";
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 { db } from "@server/db";
import { recordPing } from "@server/routers/newt/pingAccumulator";
@@ -80,6 +88,9 @@ const removeClient = async (
const updatedClients = existingClients.filter((client) => client !== ws);
if (updatedClients.length === 0) {
connectedClients.delete(mapKey);
// Remove clientId from clientConfigVersions — prevents unbounded growth
// from stale entries.
clientConfigVersions.delete(clientId);
logger.info(
`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
const getClientConfigVersion = async (clientId: string): Promise<number | undefined> => {
const getClientConfigVersion = async (
clientId: string
): Promise<number | undefined> => {
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;
};
@@ -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;
};

View File

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

View File

@@ -33,7 +33,7 @@ const CopyToClipboard = ({
<div className="flex items-center space-x-2 min-w-0 max-w-full">
<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}
>
{!copied ? (

View File

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

View File

@@ -19,7 +19,7 @@ export function InfoSections({
return (
<div
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" &&
"md:justify-items-start md:justify-start"
)}
@@ -41,7 +41,11 @@ export function InfoSection({
children: React.ReactNode;
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({
@@ -51,7 +55,11 @@ export function InfoSectionTitle({
children: React.ReactNode;
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({
@@ -62,8 +70,13 @@ export function InfoSectionContent({
className?: string;
}) {
return (
<div className={cn("min-w-0 overflow-hidden", className)}>
<div className="w-full truncate [&>div.flex]:min-w-0 [&>div.flex]:!whitespace-normal [&>div.flex>span]:truncate [&>div.flex>a]:truncate">
<div
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}
</div>
</div>

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<InfoSection>
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
<InfoSectionContent>
{resource.niceId}
<span className="inline-flex items-center">
{resource.niceId}
</span>
</InfoSectionContent>
</InfoSection>
{resource.http ? (
@@ -49,7 +51,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<InfoSectionTitle>URL</InfoSectionTitle>
<InfoSectionContent>
{resource.wildcard ? (
<span>{fullUrl}</span>
<span className="inline-flex items-center">
{fullUrl}
</span>
) : (
<CopyToClipboard
text={fullUrl}
@@ -68,7 +72,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
authInfo.sso ||
authInfo.whitelist ||
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" />
<span>{t("protected")}</span>
</div>
@@ -106,7 +110,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
{t("protocol")}
</InfoSectionTitle>
<InfoSectionContent>
{resource.protocol.toUpperCase()}
<span className="inline-flex items-center">
{resource.protocol.toUpperCase()}
</span>
</InfoSectionContent>
</InfoSection>
<InfoSection>

View File

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

View File

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

View File

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