From 17cbc2fb98aedc0b9805153672dc6ca47be9db04 Mon Sep 17 00:00:00 2001 From: KodekaTeam Date: Thu, 4 Jun 2026 02:47:38 +0700 Subject: [PATCH] fix: add parser Public Suffix List for naming domain file config and fix failed to remove old legacy file config --- package-lock.json | 19 +++++++ package.json | 1 + .../services/domain-provisioning/names.ts | 48 +++++++++++++++- .../services/domain-provisioning/nginx.ts | 55 +++++++++++++++++++ .../backend/domain-provisioning-names.test.ts | 26 +++++++++ 5 files changed, 148 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 50d903c..ec1be8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "react-dom": "19.2.4", "recharts": "^3.8.1", "tailwind-merge": "^3.5.0", + "tldts": "^7.4.2", "zod": "^4.4.3" }, "devDependencies": { @@ -11834,6 +11835,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tldts": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.2" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 0feaf7b..b70dc20 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "react-dom": "19.2.4", "recharts": "^3.8.1", "tailwind-merge": "^3.5.0", + "tldts": "^7.4.2", "zod": "^4.4.3" }, "devDependencies": { diff --git a/src/server/services/domain-provisioning/names.ts b/src/server/services/domain-provisioning/names.ts index a5ea08e..fd1a0cb 100644 --- a/src/server/services/domain-provisioning/names.ts +++ b/src/server/services/domain-provisioning/names.ts @@ -1,3 +1,5 @@ +import { getDomain } from "tldts"; + export function sanitizeDomainFileName(domainName: string): string { return domainName.toLowerCase().replace(/[^a-z0-9.-]/g, "-"); } @@ -18,7 +20,27 @@ function normalizeAnchorDomain(domainName: string): string { return domainName.trim().toLowerCase().replace(/^\*\./, ""); } -export function getDomainConfigAnchor(domainNames: string[]): string { +function resolveRegistrableDomain(domainName: string): string { + const parsed = getDomain(domainName, { + allowIcannDomains: true, + allowPrivateDomains: true, + }); + + if (parsed) { + return parsed; + } + + const labels = domainName.split(".").filter(Boolean); + if (labels.length >= 2) { + return labels.slice(-2).join("."); + } + + return domainName; +} + +export function getLegacyTwoLabelDomainConfigAnchor( + domainNames: string[], +): string { const normalized = Array.from( new Set(domainNames.map(normalizeAnchorDomain).filter(Boolean)), ); @@ -48,6 +70,30 @@ export function getDomainConfigAnchor(domainNames: string[]): string { return candidate; } +export function getDomainConfigAnchor(domainNames: string[]): string { + const normalized = Array.from( + new Set(domainNames.map(normalizeAnchorDomain).filter(Boolean)), + ); + + if (normalized.length === 0) { + return "shared-domain"; + } + + normalized.sort((left, right) => { + const leftLabels = left.split(".").length; + const rightLabels = right.split(".").length; + + if (leftLabels !== rightLabels) { + return leftLabels - rightLabels; + } + + return left.localeCompare(right); + }); + + const candidate = normalized[0] ?? "shared-domain"; + return resolveRegistrableDomain(candidate); +} + export function buildManagedNginxSharedFileBase( containerName: string, domainNames: string[], diff --git a/src/server/services/domain-provisioning/nginx.ts b/src/server/services/domain-provisioning/nginx.ts index 0f98cf2..a1a39e9 100644 --- a/src/server/services/domain-provisioning/nginx.ts +++ b/src/server/services/domain-provisioning/nginx.ts @@ -10,6 +10,7 @@ import { buildManagedNginxFileBase, buildManagedNginxSharedFileBase, getDomainConfigAnchor, + getLegacyTwoLabelDomainConfigAnchor, sanitizeDomainFileName, sanitizeContainerFileName, } from "./names"; @@ -385,6 +386,14 @@ export async function provisionNginxConfig( configMode === "SHARED" ? `${sanitizeContainerFileName(containerName)}--${sanitizeDomainFileName(getDomainConfigAnchor(domainNames))}` : null; + const legacyBuggyAnchorSafeName = + configMode === "SHARED" + ? sanitizeDomainFileName(getLegacyTwoLabelDomainConfigAnchor(domainNames)) + : null; + const legacyBuggySharedSafeName = + configMode === "SHARED" + ? `${sanitizeContainerFileName(containerName)}--${sanitizeDomainFileName(getLegacyTwoLabelDomainConfigAnchor(domainNames))}` + : null; const legacyServiceSharedSafeName = configMode === "SHARED" ? `doktainer-${sanitizeContainerFileName(containerName)}` @@ -425,6 +434,20 @@ export async function provisionNginxConfig( `rm -f ${escapeDoubleQuotedShellArg(`/etc/nginx/sites-enabled/${legacySharedSafeName}.conf`)} ${escapeDoubleQuotedShellArg(`/etc/nginx/sites-available/${legacySharedSafeName}.conf`)}`, ); } + if (legacyBuggyAnchorSafeName && legacyBuggyAnchorSafeName !== safeName) { + cleanupCommands.push( + `rm -f ${escapeDoubleQuotedShellArg(`/etc/nginx/sites-enabled/${legacyBuggyAnchorSafeName}.conf`)} ${escapeDoubleQuotedShellArg(`/etc/nginx/sites-available/${legacyBuggyAnchorSafeName}.conf`)}`, + ); + } + if ( + legacyBuggySharedSafeName && + legacyBuggySharedSafeName !== safeName && + legacyBuggySharedSafeName !== legacySharedSafeName + ) { + cleanupCommands.push( + `rm -f ${escapeDoubleQuotedShellArg(`/etc/nginx/sites-enabled/${legacyBuggySharedSafeName}.conf`)} ${escapeDoubleQuotedShellArg(`/etc/nginx/sites-available/${legacyBuggySharedSafeName}.conf`)}`, + ); + } if ( legacyServiceSharedSafeName && legacyServiceSharedSafeName !== safeName @@ -498,6 +521,14 @@ export async function removeManagedNginxProxyConfig(options: { options.containerName && configMode === "SHARED" ? `${sanitizeContainerFileName(options.containerName)}--${sanitizeDomainFileName(getDomainConfigAnchor(domainNames))}` : null; + const legacyBuggyAnchorSafeName = + configMode === "SHARED" + ? sanitizeDomainFileName(getLegacyTwoLabelDomainConfigAnchor(domainNames)) + : null; + const legacyBuggySharedSafeName = + options.containerName && configMode === "SHARED" + ? `${sanitizeContainerFileName(options.containerName)}--${sanitizeDomainFileName(getLegacyTwoLabelDomainConfigAnchor(domainNames))}` + : null; const legacyServiceSharedSafeName = options.containerName && configMode === "SHARED" ? `doktainer-${sanitizeContainerFileName(options.containerName)}` @@ -520,6 +551,30 @@ export async function removeManagedNginxProxyConfig(options: { `/etc/nginx/sites-available/${legacySharedSafeName}.conf`, ); } + if (legacyBuggyAnchorSafeName && legacyBuggyAnchorSafeName !== managedSafeName) { + await removePrivilegedFile( + options.server, + `/etc/nginx/sites-enabled/${legacyBuggyAnchorSafeName}.conf`, + ); + await removePrivilegedFile( + options.server, + `/etc/nginx/sites-available/${legacyBuggyAnchorSafeName}.conf`, + ); + } + if ( + legacyBuggySharedSafeName && + legacyBuggySharedSafeName !== managedSafeName && + legacyBuggySharedSafeName !== legacySharedSafeName + ) { + await removePrivilegedFile( + options.server, + `/etc/nginx/sites-enabled/${legacyBuggySharedSafeName}.conf`, + ); + await removePrivilegedFile( + options.server, + `/etc/nginx/sites-available/${legacyBuggySharedSafeName}.conf`, + ); + } if ( legacyServiceSharedSafeName && legacyServiceSharedSafeName !== managedSafeName diff --git a/tests/backend/domain-provisioning-names.test.ts b/tests/backend/domain-provisioning-names.test.ts index 9146569..4ad21eb 100644 --- a/tests/backend/domain-provisioning-names.test.ts +++ b/tests/backend/domain-provisioning-names.test.ts @@ -5,6 +5,7 @@ import { buildManagedNginxFileBase, buildManagedNginxSharedFileBase, getDomainConfigAnchor, + getLegacyTwoLabelDomainConfigAnchor, } from "../../src/server/services/domain-provisioning/names"; test("isolated nginx config uses a single domain file name", () => { @@ -48,3 +49,28 @@ test("shared nginx config anchor keeps service files separate across roots", () "example.com", ); }); + +test("shared nginx config anchor respects public suffix domains", () => { + assert.equal( + getDomainConfigAnchor(["stream.appku.web.id"]), + "appku.web.id", + ); + + assert.equal(getDomainConfigAnchor(["api.appku.co.id"]), "appku.co.id"); + assert.equal( + getDomainConfigAnchor(["foo.example.co.uk"]), + "example.co.uk", + ); + + assert.equal( + buildManagedNginxSharedFileBase("livestream-app", [ + "stream.appku.web.id", + ]), + "appku.web.id", + ); + + assert.equal( + getLegacyTwoLabelDomainConfigAnchor(["stream.appku.web.id"]), + "web.id", + ); +});