From f2dbb84d00b1ec8f51ca610abb52213379311931 Mon Sep 17 00:00:00 2001
From: John Coffey
Date: Fri, 6 Nov 2026 00:31:46 -0700
Subject: [PATCH 1/2] Certificates guide: keep a domain's own hostnames
The guide wrote an empty subjectAlternativeNames for every domain it
switched, so ticking a domain already on ACME wiped its custom list with
no warning. It now keeps the list, and checks and shows the names that
list orders: a name without a dot gets the domain added, one with a dot
is used as written, the same as the server.
Fixes #160
(cherry picked from commit 78813c486ff988303d8efaf1383540aeeff4f583)
---
.../certificates/CertificateSetupPage.tsx | 12 +++++++++---
src/features/certificates/names.test.ts | 7 +++++++
src/features/certificates/names.ts | 17 ++++++++++++++---
3 files changed, 30 insertions(+), 6 deletions(-)
diff --git a/src/features/certificates/CertificateSetupPage.tsx b/src/features/certificates/CertificateSetupPage.tsx
index 719b306..c7d86d3 100644
--- a/src/features/certificates/CertificateSetupPage.tsx
+++ b/src/features/certificates/CertificateSetupPage.tsx
@@ -41,6 +41,8 @@ interface DomainRow {
name: string;
automatic: boolean;
providerId?: string;
+ /** The domain's own certificate hostnames, kept as they are when it's switched. */
+ hostnames: string[];
dnsAutomatic: boolean;
zone: ReturnType;
}
@@ -93,6 +95,7 @@ async function load(): Promise {
name: String(d.name),
automatic: cm['@type'] === 'Automatic',
providerId: cm.acmeProviderId as string | undefined,
+ hostnames: cm['@type'] === 'Automatic' ? keys(cm.subjectAlternativeNames) : [],
dnsAutomatic: dm['@type'] === 'Automatic',
zone: parseZone(d.dnsZoneFile as string | undefined),
};
@@ -113,6 +116,9 @@ async function load(): Promise {
};
}
+/** "a and b" for the default pair, "a, b, c" for a domain's own list. */
+const joinNames = (names: string[]) => (names.length === 2 ? names.join(' and ') : names.join(', '));
+
/** When the switch happened, read in the click handler, for telling a new certificate from an old one. */
async function switchedAt(): Promise {
return Date.now();
@@ -206,7 +212,7 @@ export function CertificateSetupPage() {
const chosen = useMemo(() => (data ? data.domains.filter((d) => picked.has(d.id)) : []), [data, picked]);
const methodOf = (d: DomainRow): Method => (d.dnsAutomatic ? 'dns' : 'tls');
const namesOf = useCallback(
- (d: DomainRow) => orderNames(d.name, methodOf(d), data?.serverName ?? '', d.zone),
+ (d: DomainRow) => orderNames(d.name, methodOf(d), data?.serverName ?? '', d.zone, d.hostnames),
[data],
);
@@ -344,7 +350,7 @@ export function CertificateSetupPage() {
certificateManagement: {
'@type': 'Automatic',
acmeProviderId: ids[methodOf(d)],
- subjectAlternativeNames: {},
+ subjectAlternativeNames: Object.fromEntries(d.hostnames.map((h) => [h, true])),
},
},
]),
@@ -506,7 +512,7 @@ export function CertificateSetupPage() {
{t('certSetup.dnsReady', '{{domain}}: checked through its connected DNS, covering {{names}}.', {
domain: d.name,
- names: namesOf(d).join(' and '),
+ names: joinNames(namesOf(d)),
})}
))}
diff --git a/src/features/certificates/names.test.ts b/src/features/certificates/names.test.ts
index 90764d7..b3df69d 100644
--- a/src/features/certificates/names.test.ts
+++ b/src/features/certificates/names.test.ts
@@ -27,6 +27,13 @@ describe('orderNames', () => {
]);
});
+ it("uses a domain's own hostnames in place of the defaults, with either method", () => {
+ const own = ['mail', 'Example.com', 'smtp.example.com', 'mail'];
+ const want = ['mail.example.com', 'example.com', 'smtp.example.com'];
+ expect(orderNames('example.com', 'dns', 'mail.example.com', [], own)).toEqual(want);
+ expect(orderNames('example.com', 'tls', 'mail.example.com', [], own)).toEqual(want);
+ });
+
it('leaves out a server name in another domain', () => {
expect(orderNames('example.com', 'tls', 'mail.other.net')).not.toContain('mail.other.net');
});
diff --git a/src/features/certificates/names.ts b/src/features/certificates/names.ts
index 1a8fd3f..156736b 100644
--- a/src/features/certificates/names.ts
+++ b/src/features/certificates/names.ts
@@ -11,8 +11,10 @@
* The server decides the names (common/src/network/acme/order.rs): with DNS
* validation it asks for the domain and a wildcard under it; otherwise for
* mta-sts, ua-auto-config, autoconfig and autodiscover under the domain, plus
- * the server's own name when it's inside the domain. Every name must pass, or
- * the whole order fails and keeps retrying, so the guide checks them first.
+ * the server's own name when it's inside the domain. A domain's own hostname
+ * list replaces all of that: a name without a dot gets the domain added, one
+ * with a dot is used as written. Every name must pass, or the whole order
+ * fails and keeps retrying, so the guide checks them first.
*/
import type { ZoneRecord } from '@/features/dns/zone';
@@ -25,8 +27,17 @@ function inZone(name: string, domain: string): boolean {
return name === domain || name.endsWith(`.${domain}`);
}
-export function orderNames(domain: string, method: Method, serverName: string, zone: ZoneRecord[] = []): string[] {
+export function orderNames(
+ domain: string,
+ method: Method,
+ serverName: string,
+ zone: ZoneRecord[] = [],
+ hostnames: string[] = [],
+): string[] {
const d = domain.toLowerCase();
+ if (hostnames.length > 0) {
+ return [...new Set(hostnames.map((h) => (h.includes('.') ? h : `${h}.${d}`).toLowerCase()))];
+ }
if (method === 'dns') return [`*.${d}`, d];
const names = new Set(TECHNICAL_NAMES.map((h) => `${h}.${d}`));
const host = serverName.toLowerCase().replace(/\.$/, '');
--
2.54.0
From 94386d373b0b2b0a3aca7582f033110bef4d71d5 Mon Sep 17 00:00:00 2001
From: John Coffey
Date: Fri, 6 Nov 2026 01:27:13 -0700
Subject: [PATCH 2/2] Version 2026.10.26.5
---
inbuxa-version.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/inbuxa-version.json b/inbuxa-version.json
index 394b601..7b48f2f 100644
--- a/inbuxa-version.json
+++ b/inbuxa-version.json
@@ -1,3 +1,3 @@
{
- "version": "2026.10.26.4"
+ "version": "2026.10.26.5"
}
--
2.54.0