Skip to content

Commit 9810398

Browse files
committed
feat(API): gate /diagnostics chia-tools check on chia locality
The /diagnostics endpoint reports chia-tools as a recommended local tool, but chia-tools is only relevant when the Chia wallet and DataLayer run on this machine. When either RPC URL is remote, CADT can't inspect that host, so the chia-tools section now reports installed: 'unknown' (with chiaIsLocal: false) and skips the "chia-tools is recommended" warning instead of misleadingly reporting "not installed". Locality is derived from the WALLET_URL/DATALAYER_URL hosts (localhost, 127.0.0.0/8, 0.0.0.0, ::1). Adds unit tests for the host classifier and integration tests covering the remote -> unknown gating, and relaxes the live-API contract test to accept the tri-state installed value.
1 parent b1e6974 commit 9810398

3 files changed

Lines changed: 127 additions & 5 deletions

File tree

src/routes/diagnostics.js

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,32 @@ const normalizeNodeId = (id) => {
340340
return s.startsWith('0x') ? s.slice(2) : s;
341341
};
342342

343+
// Loopback hosts that mean the Chia service runs on the same machine as CADT.
344+
// Node's URL parser returns IPv6 hosts in bracketed form (e.g. "[::1]"), so we
345+
// list the bracketed form for IPv6 loopback. The whole 127.0.0.0/8 block is
346+
// loopback and handled by the prefix check in isLocalChiaUrl.
347+
const LOCAL_CHIA_HOSTNAMES = new Set([
348+
'localhost',
349+
'0.0.0.0',
350+
'[::1]',
351+
]);
352+
353+
/**
354+
* Whether a Chia RPC URL points at the local machine. Treats localhost, the
355+
* full 127.0.0.0/8 loopback block, 0.0.0.0, and IPv6 loopback as local;
356+
* anything else is remote. Malformed/empty URLs are treated as non-local
357+
* (we can't prove locality, so we don't claim it).
358+
*/
359+
const isLocalChiaUrl = (url) => {
360+
if (!url) return false;
361+
try {
362+
const hostname = new URL(url).hostname.toLowerCase();
363+
return LOCAL_CHIA_HOSTNAMES.has(hostname) || hostname.startsWith('127.');
364+
} catch {
365+
return false;
366+
}
367+
};
368+
343369
/**
344370
* Cross-reference connected wallet peers against the trusted_peers map in
345371
* the chia config.yaml. Returns a small object suitable for embedding in
@@ -688,9 +714,31 @@ export const getDiagnosticsResponse = async () => {
688714
})();
689715

690716
// ---- Chia: chia-tools / processes ---------------------------------------
691-
const chiaToolsSection = chiaToolsRes.ok
692-
? chiaToolsRes.value
693-
: { installed: false, version: null, error: chiaToolsRes.error, note: 'probe failed' };
717+
// chia-tools only needs to be installed when the Chia wallet and DataLayer
718+
// run on this machine. When either is remote, CADT can't inspect that host,
719+
// so we report installed: 'unknown' rather than a misleading "not installed".
720+
const chiaIsLocal =
721+
isLocalChiaUrl(appConfig.WALLET_URL) && isLocalChiaUrl(appConfig.DATALAYER_URL);
722+
723+
let chiaToolsSection;
724+
if (!chiaIsLocal) {
725+
chiaToolsSection = {
726+
installed: 'unknown',
727+
version: null,
728+
chiaIsLocal: false,
729+
note: 'Chia wallet/DataLayer are remote; chia-tools is not required on this host',
730+
};
731+
} else if (chiaToolsRes.ok) {
732+
chiaToolsSection = { ...chiaToolsRes.value, chiaIsLocal: true };
733+
} else {
734+
chiaToolsSection = {
735+
installed: false,
736+
version: null,
737+
chiaIsLocal: true,
738+
error: chiaToolsRes.error,
739+
note: 'probe failed',
740+
};
741+
}
694742

695743
// ---- System -------------------------------------------------------------
696744
const systemSection = systemInfoRes.ok
@@ -758,7 +806,10 @@ export const getDiagnosticsResponse = async () => {
758806
// chiaTools
759807
{
760808
const ctStatus = new StatusAccumulator();
761-
if (!chiaToolsSection.installed) {
809+
// Only nudge about installing chia-tools when Chia is local. On remote-Chia
810+
// setups the binary isn't expected on this host, so installed === 'unknown'
811+
// and we leave the status at ok.
812+
if (chiaToolsSection.chiaIsLocal && chiaToolsSection.installed !== true) {
762813
ctStatus.escalate('warning', 'chia-tools is recommended to help manage Chia');
763814
}
764815
Object.assign(chiaToolsSection, ctStatus.result());
@@ -880,6 +931,7 @@ export const __test = {
880931
collectSubscriptions,
881932
buildTrustedPeerView,
882933
normalizeNodeId,
934+
isLocalChiaUrl,
883935
collectOwnedStoreExpectations,
884936
escalateLostOwnedStores,
885937
StatusAccumulator,

tests/v2/integration/diagnostics.spec.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,32 @@ describe('/diagnostics endpoint', function () {
217217
});
218218
});
219219

220+
describe('isLocalChiaUrl', function () {
221+
it('treats loopback hosts as local', function () {
222+
const isLocal = diagnostics.__test.isLocalChiaUrl;
223+
expect(isLocal('https://localhost:9256')).to.equal(true);
224+
expect(isLocal('https://127.0.0.1:9256')).to.equal(true);
225+
expect(isLocal('https://127.0.0.2:9256')).to.equal(true);
226+
expect(isLocal('https://0.0.0.0:9256')).to.equal(true);
227+
expect(isLocal('https://[::1]:8562')).to.equal(true);
228+
});
229+
230+
it('treats non-loopback hosts as remote', function () {
231+
const isLocal = diagnostics.__test.isLocalChiaUrl;
232+
expect(isLocal('https://wallet.example.com:9256')).to.equal(false);
233+
expect(isLocal('https://10.0.0.5:9256')).to.equal(false);
234+
expect(isLocal('https://192.168.1.20:8562')).to.equal(false);
235+
});
236+
237+
it('returns false for empty or malformed input', function () {
238+
const isLocal = diagnostics.__test.isLocalChiaUrl;
239+
expect(isLocal('')).to.equal(false);
240+
expect(isLocal(null)).to.equal(false);
241+
expect(isLocal(undefined)).to.equal(false);
242+
expect(isLocal('not a url')).to.equal(false);
243+
});
244+
});
245+
220246
describe('buildTrustedPeerView', function () {
221247
const { buildTrustedPeerView } = (async () => null)(); // placeholder to keep diff small
222248
it('matches connected peers against trusted_peers regardless of 0x prefix or case', function () {
@@ -367,6 +393,43 @@ describe('/diagnostics endpoint', function () {
367393
}
368394
});
369395

396+
it('chiaTools reports chiaIsLocal=true under the default loopback config', async function () {
397+
const response = await supertest(app).get('/diagnostics').expect(200);
398+
expect(response.body.chia.chiaTools).to.have.property('chiaIsLocal', true);
399+
});
400+
401+
it('chiaTools reports installed "unknown" with ok status when Chia is remote', async function () {
402+
await withConfigOverride(async () => {
403+
const response = await supertest(app).get('/diagnostics').expect(200);
404+
const chiaTools = response.body.chia.chiaTools;
405+
expect(chiaTools.chiaIsLocal).to.equal(false);
406+
expect(chiaTools.installed).to.equal('unknown');
407+
// Remote Chia means chia-tools isn't expected locally, so no warning.
408+
expect(chiaTools.status).to.equal('ok');
409+
expect(chiaTools.note).to.include('remote');
410+
}, {
411+
APP: {
412+
WALLET_URL: 'https://wallet.example.com:9256',
413+
DATALAYER_URL: 'https://datalayer.example.com:8562',
414+
},
415+
});
416+
});
417+
418+
it('chiaTools is gated remote when only the DataLayer is remote', async function () {
419+
await withConfigOverride(async () => {
420+
const response = await supertest(app).get('/diagnostics').expect(200);
421+
const chiaTools = response.body.chia.chiaTools;
422+
expect(chiaTools.chiaIsLocal).to.equal(false);
423+
expect(chiaTools.installed).to.equal('unknown');
424+
expect(chiaTools.status).to.equal('ok');
425+
}, {
426+
APP: {
427+
WALLET_URL: 'https://localhost:9256',
428+
DATALAYER_URL: 'https://datalayer.example.com:8562',
429+
},
430+
});
431+
});
432+
370433
it('network status is ok when matches is null or true', async function () {
371434
const response = await supertest(app).get('/diagnostics').expect(200);
372435
const network = response.body.network;

tests/v2/live-api/wallet-health.live.spec.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,14 @@ describe('Wallet Health - Live', function () {
184184
expect(body.chia.runningProcesses).to.be.an('object');
185185
expect(body.chia.runningProcesses.matches).to.be.an('array');
186186
expect(body.chia.chiaTools).to.be.an('object');
187-
expect(body.chia.chiaTools.installed).to.be.a('boolean');
187+
expect(body.chia.chiaTools).to.have.property('chiaIsLocal').that.is.a('boolean');
188+
// installed is a boolean when Chia runs locally, or the string 'unknown'
189+
// when the wallet/DataLayer are remote (chia-tools can't be probed there).
190+
if (body.chia.chiaTools.chiaIsLocal) {
191+
expect(body.chia.chiaTools.installed).to.be.a('boolean');
192+
} else {
193+
expect(body.chia.chiaTools.installed).to.equal('unknown');
194+
}
188195
expect(body.chia.chiaTools.note).to.be.a('string').and.not.empty;
189196
});
190197

0 commit comments

Comments
 (0)