Files
wmantly 611f1a3318 feat(directory): real mesh-gateway count on the Multi-Site modal; docs links
"Theta Gateways: N active gateways" was counting this app's own
unrelated WireGuard roaming-client/exit-node Resources
(metadata.subType === 'wireguard') -- a completely different subsystem
from the gateway-to-gateway mesh the modal is actually about, and it
never queried jump-host's mesh registry at all (so it couldn't show
the local self-entry either, since there was nothing mesh-related
being counted in the first place).

Added utils/jump_client.js (same pattern as utils/proxy_client.js:
reuses jump-host's existing self-service jmp_ API token system rather
than inventing a new credential) to query jump-host's real
GET /api/mesh/gateways. Reports a null count (not misleading 0) when
the integration isn't configured/reachable, surfaced distinctly in the
UI. Also added help links to the published multi-site/mesh docs on the
modal.

Includes docs links + count only -- this session also discovered that
utils/proxy_client.js's PROXY_INTERNAL_URL, and now JUMP_INTERNAL_URL,
were never actually wired into theta-suite's docker-compose.yml, so
both service-to-service integrations were unreachable in every real
deployment despite existing in code (fixed in theta-suite separately).
2026-08-10 22:17:57 -04:00

82 lines
3.0 KiB
JavaScript

'use strict';
// Service-to-service client for jump-host's mesh registry -- used by the
// Directory's Multi-Site & Network Gateway Status modal to show the real
// number of gateway-to-gateway WireGuard mesh peers (see MULTI_SITE_SPEC.md),
// instead of counting the unrelated older WireGuard roaming-client/exit-node
// Resources in this app's own catalog (a different subsystem entirely --
// api_directory_admin.js used to filter Resource.list() for
// metadata.subType === 'wireguard', which has nothing to do with the mesh).
//
// Same pattern as utils/proxy_client.js: reuses jump-host's existing
// self-service API token system (models/api_token.js, `jmp_<id>_<secret>`
// bearer tokens) rather than inventing a new credential type. The token must
// be minted by a jump-admin user (GET /api/mesh/gateways requires
// requireJumpAdmin, which checks the token's creator's username/groups, not
// anything the token itself carries) and stored in OpenBao.
const baoConf = require('@simpleworkjs/bao-conf');
const PATH = 'integrations/theta-jump'; // baoConf adds the secret/data prefix
const REQUEST_TIMEOUT_MS = 10000;
let cachedToken = null;
async function loadToken() {
if (cachedToken) return cachedToken;
let stored;
try {
stored = await baoConf.get(PATH);
} catch (err) {
console.error(`[jump_client] could not read ${PATH} from OpenBao: ${err.message}`);
return null;
}
if (!stored || !stored.token) return null;
cachedToken = stored.token;
return cachedToken;
}
function jumpBaseUrl() {
// Not OpenBao -- this is where jump-host's admin API lives, not a secret.
return process.env.JUMP_INTERNAL_URL || '';
}
// Returns { count, note }. count is null (not 0) when the query couldn't run
// at all (not configured, unreachable, unauthorized) -- the modal shows a
// count of gateways it could actually see, not a misleading "0" that reads
// as "you have no mesh peers" when the truth is "this isn't wired up yet".
async function getGatewayCount() {
const base = jumpBaseUrl();
if (!base) {
return { count: null, note: 'skipped: JUMP_INTERNAL_URL not configured' };
}
const token = await loadToken();
if (!token) {
return { count: null, note: `skipped: no jump-host API token at OpenBao ${PATH} -- mint one on jump-host (as a jump-admin user) and store it there` };
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
try {
const resp = await fetch(base.replace(/\/+$/, '') + '/api/mesh/gateways', {
headers: { Authorization: 'Bearer ' + token },
signal: controller.signal
});
if (!resp.ok) {
return { count: null, note: `failed: HTTP ${resp.status}` };
}
const body = await resp.json();
const gateways = Array.isArray(body.gateways) ? body.gateways : [];
return { count: gateways.length, note: 'ok' };
} catch (err) {
return { count: null, note: `failed: ${err.message}` };
} finally {
clearTimeout(timer);
}
}
// Test seam.
function _reset() { cachedToken = null; }
module.exports = { getGatewayCount, _reset, PATH };