From f89fc575aad841243405a858d5cb8b7821075b92 Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:44:39 +0200 Subject: [PATCH 1/2] fix(domains): skip non-public-TLD bases in boot seeding + prune lingering pending rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Non-public bases (.internal/.lan/...) can never verify against public DNS, so they lingered as permanent 'pending' noise on the Domains page once B made them visible. Now they are never seeded, and existing auto-seeded pending non-public rows are pruned on boot. Verified rows are never touched; routes read routes.domain directly and nothing FKs to the domains table. Follow-up to the A→C→B domain initiative (B = PR #185). --- src/services/domainBoot.js | 14 +++++++++++++- tests/domain_boot_seed.test.js | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/services/domainBoot.js b/src/services/domainBoot.js index 5036b248..06fd470d 100644 --- a/src/services/domainBoot.js +++ b/src/services/domainBoot.js @@ -4,6 +4,7 @@ const { getDb } = require('../db/connection'); const domains = require('./domains'); const settings = require('./settings'); const { extractBaseDomains, shouldFlagServerIp } = require('./domainSeed'); +const { isPublicDomain } = require('./caddyTlsAutomation'); /** * Verify a set of domain names against the server IP, persist results, and @@ -34,9 +35,20 @@ async function verifyAndReflag(domainNames, { verifyEach = domains.verify } = {} async function runDomainSeedAndVerify({ verifyEach = domains.verify } = {}) { const routeDomains = getDb().prepare('SELECT DISTINCT domain FROM routes WHERE domain IS NOT NULL').all().map(r => r.domain); - const bases = extractBaseDomains(routeDomains); + // Only public-TLD bases can ever be verified against public DNS. Non-public + // bases (.internal/.lan/...) would otherwise linger forever as 'pending' + // noise on the Domains page, so they are never seeded. + const bases = extractBaseDomains(routeDomains).filter(isPublicDomain); for (const d of bases) domains.seedPending(d); + // One-time cleanup: drop non-public bases that earlier boots auto-seeded as + // 'pending' (they can never verify; routes consume routes.domain directly and + // nothing references the domains table by FK). Verified rows are never touched. + const lingering = getDb().prepare("SELECT domain FROM domains WHERE status='pending'").all().map(r => r.domain); + for (const d of lingering) { + if (!isPublicDomain(d)) getDb().prepare('DELETE FROM domains WHERE domain=?').run(d); + } + // verify only rows still pending (idempotent across boots) const pending = getDb().prepare("SELECT domain FROM domains WHERE status='pending'").all().map(r => r.domain); const { verified, flagged } = await verifyAndReflag(pending, { verifyEach }); diff --git a/tests/domain_boot_seed.test.js b/tests/domain_boot_seed.test.js index 2f291fdb..0f10f7f8 100644 --- a/tests/domain_boot_seed.test.js +++ b/tests/domain_boot_seed.test.js @@ -28,6 +28,27 @@ test('seeds distinct base domains and verifies them', async () => { assert.ok(rows.every(r => r.status === 'verified')); }); +test('skips non-public-TLD bases and prunes lingering pending non-public rows', async () => { + // an internal-TLD route: its base (gc.internal) must NEVER be seeded + getDb().prepare(`INSERT INTO routes (description, domain, target_ip, target_port, enabled, route_type) + VALUES ('r3','nas.gc.internal','10.0.0.4','80',1,'http')`).run(); + // a non-public base auto-seeded by an earlier boot → must be pruned + getDb().prepare("INSERT OR IGNORE INTO domains (domain, status) VALUES ('old.lan','pending')").run(); + // a verified row must survive — cleanup only targets pending rows + getDb().prepare("INSERT OR IGNORE INTO domains (domain, status) VALUES ('kept.internal','verified')").run(); + + const res = await domainBoot.runDomainSeedAndVerify({ + verifyEach: async (d) => ({ status: 'verified', resolvedIp: '1.2.3.4', expectedIp: '1.2.3.4', error: null }), + }); + + const rows = getDb().prepare('SELECT domain FROM domains ORDER BY domain').all().map(r => r.domain); + assert.equal(res.seeded, 2, 'only the 2 public bases are seeded'); + assert.ok(!rows.includes('gc.internal'), 'non-public base must not be seeded'); + assert.ok(!rows.includes('old.lan'), 'lingering pending non-public row must be pruned'); + assert.ok(rows.includes('kept.internal'), 'verified rows must never be pruned'); + assert.ok(rows.includes('domaincaster.com') && rows.includes('marcbackes.net'), 'public bases seeded'); +}); + test('>=2 all-mismatch sets the server-IP warning and keeps rows pending', async () => { const res = await domainBoot.runDomainSeedAndVerify({ verifyEach: async (d) => ({ status: 'failed', resolvedIp: '9.9.9.9', expectedIp: '1.2.3.4', error: 'x' }), From 9fa2c9cee130d732c21b332e92cc36bd9b3baeba Mon Sep 17 00:00:00 2001 From: CallMeTechie <34693633+CallMeTechie@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:49:35 +0200 Subject: [PATCH 2/2] test(domains): use Set.has to dodge CodeQL url-substring FP; prepare delete stmt once --- src/services/domainBoot.js | 3 ++- tests/domain_boot_seed.test.js | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/services/domainBoot.js b/src/services/domainBoot.js index 06fd470d..4d3ddd79 100644 --- a/src/services/domainBoot.js +++ b/src/services/domainBoot.js @@ -45,8 +45,9 @@ async function runDomainSeedAndVerify({ verifyEach = domains.verify } = {}) { // 'pending' (they can never verify; routes consume routes.domain directly and // nothing references the domains table by FK). Verified rows are never touched. const lingering = getDb().prepare("SELECT domain FROM domains WHERE status='pending'").all().map(r => r.domain); + const delStmt = getDb().prepare('DELETE FROM domains WHERE domain=?'); for (const d of lingering) { - if (!isPublicDomain(d)) getDb().prepare('DELETE FROM domains WHERE domain=?').run(d); + if (!isPublicDomain(d)) delStmt.run(d); } // verify only rows still pending (idempotent across boots) diff --git a/tests/domain_boot_seed.test.js b/tests/domain_boot_seed.test.js index 0f10f7f8..15438266 100644 --- a/tests/domain_boot_seed.test.js +++ b/tests/domain_boot_seed.test.js @@ -41,12 +41,14 @@ test('skips non-public-TLD bases and prunes lingering pending non-public rows', verifyEach: async (d) => ({ status: 'verified', resolvedIp: '1.2.3.4', expectedIp: '1.2.3.4', error: null }), }); - const rows = getDb().prepare('SELECT domain FROM domains ORDER BY domain').all().map(r => r.domain); + // Set membership (exact match) — avoids CodeQL's js/incomplete-url-substring + // false-positive that fires on Array.includes('host.tld') in tests. + const present = new Set(getDb().prepare('SELECT domain FROM domains').all().map(r => r.domain)); assert.equal(res.seeded, 2, 'only the 2 public bases are seeded'); - assert.ok(!rows.includes('gc.internal'), 'non-public base must not be seeded'); - assert.ok(!rows.includes('old.lan'), 'lingering pending non-public row must be pruned'); - assert.ok(rows.includes('kept.internal'), 'verified rows must never be pruned'); - assert.ok(rows.includes('domaincaster.com') && rows.includes('marcbackes.net'), 'public bases seeded'); + assert.ok(!present.has('gc.internal'), 'non-public base must not be seeded'); + assert.ok(!present.has('old.lan'), 'lingering pending non-public row must be pruned'); + assert.ok(present.has('kept.internal'), 'verified rows must never be pruned'); + assert.ok(present.has('domaincaster.com') && present.has('marcbackes.net'), 'public bases seeded'); }); test('>=2 all-mismatch sets the server-IP warning and keeps rows pending', async () => {