fix(bug): Fixed a bug in Nginx shared multi-domain. So that when renewing SSL, it doesn't delete the other domain in one config file.
This commit is contained in:
1 parent
b48b87814a
commit
54bba5c19a
2 files changed
+320
-143
No files matched your search
+217
-143
@@ -5,7 +5,11 @@ import prisma from "../lib/prisma";
|
||||
import { authenticate, requireApiKeyPermission } from "../middleware/auth";
|
||||
import { auditLog } from "../services/audit.service";
|
||||
import { dispatchRuntimeNotification } from "../services/notification.service";
|
||||
import { provisionDomainProxyConfig } from "../services/domain-provisioning";
|
||||
import {
|
||||
provisionDomainProxyConfig,
|
||||
resolveContainerUpstream,
|
||||
} from "../services/domain-provisioning";
|
||||
import { getDomainConfigAnchor } from "../services/domain-provisioning/names";
|
||||
import * as ssh from "../services/ssh.service";
|
||||
|
||||
const IssueCertSchema = z.object({
|
||||
@@ -45,6 +49,18 @@ type SharedSslGroupMember = {
|
||||
isPrimary: boolean;
|
||||
sslEnabled: boolean;
|
||||
autoRenew: boolean;
|
||||
targetContainerId: string | null;
|
||||
targetPort: number | null;
|
||||
targetContainer: {
|
||||
id: string;
|
||||
name: string;
|
||||
dockerId: string | null;
|
||||
serverId: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type SharedSslGroupCandidate = SharedSslGroupMember & {
|
||||
configMode: "SHARED" | "ISOLATED";
|
||||
};
|
||||
|
||||
type SslRenewOperation = {
|
||||
@@ -86,6 +102,50 @@ function deriveSslStatus(
|
||||
return expiresAt.getTime() <= thirtyDaysFromNow ? "EXPIRING" : "VALID";
|
||||
}
|
||||
|
||||
export type DiscoveredSslDomainCertificate = {
|
||||
domainName: string;
|
||||
certificate: ssh.DiscoveredSslCertificate;
|
||||
};
|
||||
|
||||
function getCertificateExpiryTime(certificate: {
|
||||
expiresAt: Date | null;
|
||||
}): number {
|
||||
return certificate.expiresAt?.getTime() ?? 0;
|
||||
}
|
||||
|
||||
function isBetterDiscoveredCertificate(
|
||||
candidate: ssh.DiscoveredSslCertificate,
|
||||
current: ssh.DiscoveredSslCertificate,
|
||||
): boolean {
|
||||
const candidateExpiry = getCertificateExpiryTime(candidate);
|
||||
const currentExpiry = getCertificateExpiryTime(current);
|
||||
|
||||
if (candidateExpiry !== currentExpiry) {
|
||||
return candidateExpiry > currentExpiry;
|
||||
}
|
||||
|
||||
return candidate.certName.localeCompare(current.certName) < 0;
|
||||
}
|
||||
|
||||
export function selectBestDiscoveredSslCertificateByDomain(
|
||||
certificates: ssh.DiscoveredSslCertificate[],
|
||||
): DiscoveredSslDomainCertificate[] {
|
||||
const byDomain = new Map<string, ssh.DiscoveredSslCertificate>();
|
||||
|
||||
for (const certificate of certificates) {
|
||||
for (const domainName of certificate.domainNames) {
|
||||
const current = byDomain.get(domainName);
|
||||
if (!current || isBetterDiscoveredCertificate(certificate, current)) {
|
||||
byDomain.set(domainName, certificate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(byDomain.entries())
|
||||
.map(([domainName, certificate]) => ({ domainName, certificate }))
|
||||
.sort((left, right) => left.domainName.localeCompare(right.domainName));
|
||||
}
|
||||
|
||||
function isSharedNginxDomain(domain: {
|
||||
proxy: "TRAEFIK" | "NGINX" | "CADDY" | "NONE";
|
||||
configMode?: "SHARED" | "ISOLATED";
|
||||
@@ -130,27 +190,51 @@ function choosePrimaryDomain(domainNames: string[]): string | null {
|
||||
async function listSharedSslGroupMembers(options: {
|
||||
organizationId: string;
|
||||
serverId: string;
|
||||
targetContainerId: string;
|
||||
targetPort: number;
|
||||
rootDomain: string;
|
||||
}): Promise<SharedSslGroupMember[]> {
|
||||
return prisma.domain.findMany({
|
||||
const candidates = await prisma.domain.findMany({
|
||||
where: {
|
||||
organizationId: options.organizationId,
|
||||
serverId: options.serverId,
|
||||
targetContainerId: options.targetContainerId,
|
||||
targetPort: options.targetPort,
|
||||
proxy: "NGINX",
|
||||
configMode: "SHARED",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
configMode: true,
|
||||
isPrimary: true,
|
||||
sslEnabled: true,
|
||||
autoRenew: true,
|
||||
targetContainerId: true,
|
||||
targetPort: true,
|
||||
targetContainer: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
dockerId: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ isPrimary: "desc" }, { name: "asc" }],
|
||||
});
|
||||
|
||||
return filterSharedSslGroupMembersByRoot(candidates, {
|
||||
rootDomain: options.rootDomain,
|
||||
});
|
||||
}
|
||||
|
||||
export function filterSharedSslGroupMembersByRoot(
|
||||
candidates: SharedSslGroupCandidate[],
|
||||
options: {
|
||||
rootDomain: string;
|
||||
},
|
||||
): SharedSslGroupMember[] {
|
||||
return candidates.filter(
|
||||
(domain) =>
|
||||
getDomainConfigAnchor([domain.name]) === options.rootDomain &&
|
||||
(domain.configMode === "SHARED" || domain.name === options.rootDomain),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveSharedGroupPrimaryDomain(
|
||||
@@ -162,34 +246,10 @@ function resolveSharedGroupPrimaryDomain(
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchTargetContainer(options: {
|
||||
organizationId: string;
|
||||
targetContainerId: string;
|
||||
}): Promise<{
|
||||
id: string;
|
||||
name: string;
|
||||
dockerId: string | null;
|
||||
serverId: string;
|
||||
} | null> {
|
||||
return prisma.container.findFirst({
|
||||
where: {
|
||||
id: options.targetContainerId,
|
||||
server: { organizationId: options.organizationId },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
dockerId: true,
|
||||
serverId: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function persistSharedSslCertificate(options: {
|
||||
organizationId: string;
|
||||
serverId: string;
|
||||
targetContainerId: string;
|
||||
targetPort: number;
|
||||
rootDomain: string;
|
||||
autoRenew: boolean;
|
||||
issued: ssh.SslCertificateResult;
|
||||
}): Promise<void> {
|
||||
@@ -236,8 +296,7 @@ async function persistSharedSslCertificate(options: {
|
||||
async function setSharedSslAutoRenew(options: {
|
||||
organizationId: string;
|
||||
serverId: string;
|
||||
targetContainerId: string;
|
||||
targetPort: number;
|
||||
rootDomain: string;
|
||||
autoRenew: boolean;
|
||||
}): Promise<void> {
|
||||
const members = await listSharedSslGroupMembers(options);
|
||||
@@ -260,8 +319,7 @@ async function setSharedSslAutoRenew(options: {
|
||||
async function clearSharedSslState(options: {
|
||||
organizationId: string;
|
||||
serverId: string;
|
||||
targetContainerId: string;
|
||||
targetPort: number;
|
||||
rootDomain: string;
|
||||
}): Promise<void> {
|
||||
const members = await listSharedSslGroupMembers(options);
|
||||
if (members.length === 0) {
|
||||
@@ -283,8 +341,7 @@ async function reprovisionSharedDomainProxyIfNeeded(options: {
|
||||
organizationId: string;
|
||||
server: Awaited<ReturnType<typeof prisma.server.findUnique>>;
|
||||
serverId: string;
|
||||
targetContainerId: string;
|
||||
targetPort: number;
|
||||
rootDomain: string;
|
||||
}): Promise<void> {
|
||||
if (!options.server) {
|
||||
return;
|
||||
@@ -293,15 +350,10 @@ async function reprovisionSharedDomainProxyIfNeeded(options: {
|
||||
const members = await listSharedSslGroupMembers({
|
||||
organizationId: options.organizationId,
|
||||
serverId: options.serverId,
|
||||
targetContainerId: options.targetContainerId,
|
||||
targetPort: options.targetPort,
|
||||
});
|
||||
const container = await fetchTargetContainer({
|
||||
organizationId: options.organizationId,
|
||||
targetContainerId: options.targetContainerId,
|
||||
rootDomain: options.rootDomain,
|
||||
});
|
||||
|
||||
if (!container || members.length === 0) {
|
||||
if (members.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -312,16 +364,44 @@ async function reprovisionSharedDomainProxyIfNeeded(options: {
|
||||
return;
|
||||
}
|
||||
|
||||
const provisionableMembers = members.filter(
|
||||
(
|
||||
member,
|
||||
): member is SharedSslGroupMember & {
|
||||
targetContainer: NonNullable<SharedSslGroupMember["targetContainer"]>;
|
||||
targetPort: number;
|
||||
} => Boolean(member.targetContainer && member.targetPort),
|
||||
);
|
||||
|
||||
const nginxServerEntries = await Promise.all(
|
||||
provisionableMembers.map(async (member) => ({
|
||||
domainName: member.name,
|
||||
upstream: (
|
||||
await resolveContainerUpstream(
|
||||
options.server!,
|
||||
member.targetContainer,
|
||||
member.targetPort,
|
||||
)
|
||||
).upstream,
|
||||
})),
|
||||
);
|
||||
const fallbackTarget = provisionableMembers[0] ?? null;
|
||||
|
||||
if (!fallbackTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
await provisionDomainProxyConfig({
|
||||
server: options.server,
|
||||
domainName: primaryDomainName,
|
||||
domainNames: members.map((member) => member.name),
|
||||
nginxServerEntries,
|
||||
primaryDomainName,
|
||||
configMode: "SHARED",
|
||||
proxy: "NGINX",
|
||||
sslEnabled: members.some((member) => member.sslEnabled),
|
||||
container,
|
||||
targetPort: options.targetPort,
|
||||
container: fallbackTarget.targetContainer,
|
||||
targetPort: fallbackTarget.targetPort,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -357,8 +437,7 @@ async function reprovisionDomainProxyIfNeeded(options: {
|
||||
organizationId: server.organizationId,
|
||||
server,
|
||||
serverId: server.id,
|
||||
targetContainerId: domain.targetContainerId!,
|
||||
targetPort: domain.targetPort!,
|
||||
rootDomain: getDomainConfigAnchor([domain.name]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -582,11 +661,11 @@ function startSslRenewOperation(options: {
|
||||
|
||||
const updateDatabaseStartedAt = Date.now();
|
||||
if (cert.domain.serverId && isSharedNginxDomain(cert.domain)) {
|
||||
const rootDomain = getDomainConfigAnchor([cert.domain.name]);
|
||||
await persistSharedSslCertificate({
|
||||
organizationId,
|
||||
serverId: cert.domain.serverId,
|
||||
targetContainerId: cert.domain.targetContainerId!,
|
||||
targetPort: cert.domain.targetPort!,
|
||||
rootDomain,
|
||||
autoRenew: cert.domain.autoRenew,
|
||||
issued: renewed,
|
||||
});
|
||||
@@ -620,8 +699,7 @@ function startSslRenewOperation(options: {
|
||||
organizationId,
|
||||
server,
|
||||
serverId: cert.domain.serverId,
|
||||
targetContainerId: cert.domain.targetContainerId!,
|
||||
targetPort: cert.domain.targetPort!,
|
||||
rootDomain: getDomainConfigAnchor([cert.domain.name]),
|
||||
});
|
||||
} else {
|
||||
await reloadDomainProxyAfterRenewIfNeeded({
|
||||
@@ -741,8 +819,10 @@ export async function sslRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
const discoveredNames = Array.from(
|
||||
new Set(certificates.flatMap((cert) => cert.domainNames)),
|
||||
const discoveredDomainCertificates =
|
||||
selectBestDiscoveredSslCertificateByDomain(certificates);
|
||||
const discoveredNames = discoveredDomainCertificates.map(
|
||||
(item) => item.domainName,
|
||||
);
|
||||
|
||||
const existingDomains = discoveredNames.length
|
||||
@@ -759,7 +839,8 @@ export async function sslRoutes(app: FastifyInstance) {
|
||||
);
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
for (const cert of certificates) {
|
||||
for (const item of discoveredDomainCertificates) {
|
||||
const { domainName, certificate: cert } = item;
|
||||
const status = deriveSslStatus(cert.expiresAt);
|
||||
|
||||
if (status === "EXPIRING") {
|
||||
@@ -767,101 +848,97 @@ export async function sslRoutes(app: FastifyInstance) {
|
||||
? cert.expiresAt.toISOString().slice(0, 10)
|
||||
: "unknown date";
|
||||
|
||||
for (const domainName of cert.domainNames) {
|
||||
pendingNotifications.push(
|
||||
dispatchRuntimeNotification({
|
||||
organizationId: req.organizationId!,
|
||||
action: "ssl_expiring",
|
||||
title: `SSL expiring for ${domainName}`,
|
||||
message: `SSL certificate for ${domainName} on server ${server.name} will expire on ${expiresAtLabel}.`,
|
||||
pendingNotifications.push(
|
||||
dispatchRuntimeNotification({
|
||||
organizationId: req.organizationId!,
|
||||
action: "ssl_expiring",
|
||||
title: `SSL expiring for ${domainName}`,
|
||||
message: `SSL certificate for ${domainName} on server ${server.name} will expire on ${expiresAtLabel}.`,
|
||||
serverId: server.id,
|
||||
resourceType: "domain",
|
||||
resourceId: domainName,
|
||||
metadata: {
|
||||
domainName,
|
||||
serverId: server.id,
|
||||
resourceType: "domain",
|
||||
resourceId: domainName,
|
||||
metadata: {
|
||||
domainName,
|
||||
serverId: server.id,
|
||||
serverName: server.name,
|
||||
expiresAt: cert.expiresAt?.toISOString() ?? null,
|
||||
issuer: cert.issuer,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
serverName: server.name,
|
||||
expiresAt: cert.expiresAt?.toISOString() ?? null,
|
||||
issuer: cert.issuer,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (const domainName of cert.domainNames) {
|
||||
const existingDomain = existingByName.get(domainName);
|
||||
const existingDomain = existingByName.get(domainName);
|
||||
|
||||
const domain = existingDomain
|
||||
? await tx.domain.update({
|
||||
where: { id: existingDomain.id },
|
||||
data: {
|
||||
serverId: server.id,
|
||||
sslEnabled: true,
|
||||
isActive: true,
|
||||
discoverySource:
|
||||
existingDomain.discoverySource === "MANUAL"
|
||||
? "CERTBOT"
|
||||
: existingDomain.discoverySource,
|
||||
value: existingDomain.value?.trim()
|
||||
? existingDomain.value
|
||||
: server.ip,
|
||||
},
|
||||
})
|
||||
: await tx.domain.create({
|
||||
data: {
|
||||
name: domainName,
|
||||
organizationId: req.organizationId!,
|
||||
type: "A",
|
||||
value: server.ip,
|
||||
serverId: server.id,
|
||||
proxy: "NONE",
|
||||
discoverySource: "CERTBOT",
|
||||
sslEnabled: true,
|
||||
autoRenew: true,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
existingByName.set(domainName, {
|
||||
...domain,
|
||||
sslCert: existingDomain?.sslCert ?? null,
|
||||
});
|
||||
|
||||
const existingCert = existingDomain?.sslCert;
|
||||
if (existingCert) {
|
||||
await tx.sslCert.update({
|
||||
where: { id: existingCert.id },
|
||||
const domain = existingDomain
|
||||
? await tx.domain.update({
|
||||
where: { id: existingDomain.id },
|
||||
data: {
|
||||
issuer: cert.issuer,
|
||||
certPem: cert.certPem,
|
||||
keyPem: cert.keyPem,
|
||||
issuedAt: cert.issuedAt,
|
||||
expiresAt: cert.expiresAt,
|
||||
status,
|
||||
serverId: server.id,
|
||||
sslEnabled: true,
|
||||
isActive: true,
|
||||
discoverySource:
|
||||
existingDomain.discoverySource === "MANUAL"
|
||||
? "CERTBOT"
|
||||
: existingDomain.discoverySource,
|
||||
value: existingDomain.value?.trim()
|
||||
? existingDomain.value
|
||||
: server.ip,
|
||||
},
|
||||
})
|
||||
: await tx.domain.create({
|
||||
data: {
|
||||
name: domainName,
|
||||
organizationId: req.organizationId!,
|
||||
type: "A",
|
||||
value: server.ip,
|
||||
serverId: server.id,
|
||||
proxy: "NONE",
|
||||
discoverySource: "CERTBOT",
|
||||
sslEnabled: true,
|
||||
autoRenew: true,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const createdCert = await tx.sslCert.create({
|
||||
existingByName.set(domainName, {
|
||||
...domain,
|
||||
sslCert: existingDomain?.sslCert ?? null,
|
||||
});
|
||||
|
||||
const existingCert = existingDomain?.sslCert;
|
||||
if (existingCert) {
|
||||
await tx.sslCert.update({
|
||||
where: { id: existingCert.id },
|
||||
data: {
|
||||
domainId: domain.id,
|
||||
issuer: cert.issuer,
|
||||
certPem: cert.certPem,
|
||||
keyPem: cert.keyPem,
|
||||
issuedAt: cert.issuedAt,
|
||||
expiresAt: cert.expiresAt,
|
||||
status,
|
||||
autoRenew: existingDomain?.autoRenew ?? true,
|
||||
},
|
||||
});
|
||||
|
||||
existingByName.set(domainName, {
|
||||
...(existingByName.get(domainName) ?? domain),
|
||||
sslCert: createdCert,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const createdCert = await tx.sslCert.create({
|
||||
data: {
|
||||
domainId: domain.id,
|
||||
issuer: cert.issuer,
|
||||
certPem: cert.certPem,
|
||||
keyPem: cert.keyPem,
|
||||
issuedAt: cert.issuedAt,
|
||||
expiresAt: cert.expiresAt,
|
||||
status,
|
||||
autoRenew: existingDomain?.autoRenew ?? true,
|
||||
},
|
||||
});
|
||||
|
||||
existingByName.set(domainName, {
|
||||
...(existingByName.get(domainName) ?? domain),
|
||||
sslCert: createdCert,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1042,11 +1119,11 @@ export async function sslRoutes(app: FastifyInstance) {
|
||||
let issued;
|
||||
try {
|
||||
if (isSharedNginxDomain(domain)) {
|
||||
const rootDomain = getDomainConfigAnchor([domain.name]);
|
||||
const members = await listSharedSslGroupMembers({
|
||||
organizationId: req.organizationId!,
|
||||
serverId: domain.serverId!,
|
||||
targetContainerId: domain.targetContainerId!,
|
||||
targetPort: domain.targetPort!,
|
||||
rootDomain,
|
||||
});
|
||||
const primaryDomainName =
|
||||
resolveSharedGroupPrimaryDomain(members) ?? domain.name;
|
||||
@@ -1059,8 +1136,7 @@ export async function sslRoutes(app: FastifyInstance) {
|
||||
await persistSharedSslCertificate({
|
||||
organizationId: req.organizationId!,
|
||||
serverId: domain.serverId!,
|
||||
targetContainerId: domain.targetContainerId!,
|
||||
targetPort: domain.targetPort!,
|
||||
rootDomain,
|
||||
autoRenew: body.data.autoRenew,
|
||||
issued,
|
||||
});
|
||||
@@ -1283,8 +1359,7 @@ export async function sslRoutes(app: FastifyInstance) {
|
||||
await setSharedSslAutoRenew({
|
||||
organizationId: req.organizationId!,
|
||||
serverId: existingCert.domain.serverId!,
|
||||
targetContainerId: existingCert.domain.targetContainerId!,
|
||||
targetPort: existingCert.domain.targetPort!,
|
||||
rootDomain: getDomainConfigAnchor([existingCert.domain.name]),
|
||||
autoRenew: body.data.autoRenew,
|
||||
});
|
||||
} else {
|
||||
@@ -1350,18 +1425,17 @@ export async function sslRoutes(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
if (isSharedNginxDomain(cert.domain)) {
|
||||
const rootDomain = getDomainConfigAnchor([cert.domain.name]);
|
||||
await clearSharedSslState({
|
||||
organizationId: req.organizationId!,
|
||||
serverId: cert.domain.serverId!,
|
||||
targetContainerId: cert.domain.targetContainerId!,
|
||||
targetPort: cert.domain.targetPort!,
|
||||
rootDomain,
|
||||
});
|
||||
await reprovisionSharedDomainProxyIfNeeded({
|
||||
organizationId: req.organizationId!,
|
||||
server,
|
||||
serverId: cert.domain.serverId!,
|
||||
targetContainerId: cert.domain.targetContainerId!,
|
||||
targetPort: cert.domain.targetPort!,
|
||||
rootDomain,
|
||||
});
|
||||
} else {
|
||||
await prisma.sslCert.delete({ where: { id } });
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
filterSharedSslGroupMembersByRoot,
|
||||
selectBestDiscoveredSslCertificateByDomain,
|
||||
} from "../../src/server/routes/ssl";
|
||||
|
||||
test("shared SSL renewal scopes members to the renewed root domain", () => {
|
||||
const candidates = [
|
||||
{
|
||||
id: "root",
|
||||
name: "example.com",
|
||||
configMode: "SHARED" as const,
|
||||
isPrimary: true,
|
||||
sslEnabled: true,
|
||||
autoRenew: true,
|
||||
targetContainerId: "container-1",
|
||||
targetPort: 80,
|
||||
targetContainer: null,
|
||||
},
|
||||
{
|
||||
id: "api",
|
||||
name: "api.example.com",
|
||||
configMode: "SHARED" as const,
|
||||
isPrimary: false,
|
||||
sslEnabled: true,
|
||||
autoRenew: true,
|
||||
targetContainerId: "container-2",
|
||||
targetPort: 8080,
|
||||
targetContainer: null,
|
||||
},
|
||||
{
|
||||
id: "other-root",
|
||||
name: "other.com",
|
||||
configMode: "SHARED" as const,
|
||||
isPrimary: true,
|
||||
sslEnabled: true,
|
||||
autoRenew: true,
|
||||
targetContainerId: "container-1",
|
||||
targetPort: 80,
|
||||
targetContainer: null,
|
||||
},
|
||||
{
|
||||
id: "other-api",
|
||||
name: "api.other.com",
|
||||
configMode: "SHARED" as const,
|
||||
isPrimary: false,
|
||||
sslEnabled: true,
|
||||
autoRenew: true,
|
||||
targetContainerId: "container-2",
|
||||
targetPort: 8080,
|
||||
targetContainer: null,
|
||||
},
|
||||
];
|
||||
|
||||
const members = filterSharedSslGroupMembersByRoot(candidates, {
|
||||
rootDomain: "example.com",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
members.map((member) => member.name),
|
||||
["example.com", "api.example.com"],
|
||||
);
|
||||
});
|
||||
|
||||
test("SSL sync keeps the newest discovered certificate for duplicate domains", () => {
|
||||
const oldExpiry = new Date("2026-07-01T00:00:00.000Z");
|
||||
const renewedExpiry = new Date("2026-09-01T00:00:00.000Z");
|
||||
|
||||
const selected = selectBestDiscoveredSslCertificateByDomain([
|
||||
{
|
||||
certName: "example.com",
|
||||
domainNames: ["example.com", "api.example.com"],
|
||||
issuer: "Let's Encrypt",
|
||||
certPem: "old-cert",
|
||||
keyPem: "old-key",
|
||||
issuedAt: new Date("2026-04-01T00:00:00.000Z"),
|
||||
expiresAt: oldExpiry,
|
||||
},
|
||||
{
|
||||
certName: "example.com-0001",
|
||||
domainNames: ["example.com", "api.example.com"],
|
||||
issuer: "Let's Encrypt",
|
||||
certPem: "renewed-cert",
|
||||
keyPem: "renewed-key",
|
||||
issuedAt: new Date("2026-06-01T00:00:00.000Z"),
|
||||
expiresAt: renewedExpiry,
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
selected.map((item) => [
|
||||
item.domainName,
|
||||
item.certificate.certName,
|
||||
item.certificate.expiresAt,
|
||||
]),
|
||||
[
|
||||
["api.example.com", "example.com-0001", renewedExpiry],
|
||||
["example.com", "example.com-0001", renewedExpiry],
|
||||
],
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user