Files
memind/admin-guard.test.mjs
T
john b5c600ad81 Split back-office into standalone memind_adm service
Carve the platform super-admin API (/admin-api) and plaza operations console
API (/api/ops/v1) out of the public server.mjs into their own process, so the
user-facing fleet can no longer be taken down or scaled by back-office traffic.

Architecture (split-ready, single process for now):
- admin-routes.mjs: createAdminApi / createOpsApi route factories (DI, single
  source of truth; the route logic moved verbatim out of server.mjs).
- admin-bootstrap.mjs: lean service container (user-auth, LLM providers, minimal
  plaza graph) with no user-facing daemons.
- admin-server.mjs: standalone entry with a console registry. ADMIN_CONSOLES
  selects which consoles a process mounts (default both), so splitting into two
  processes later is a config change, not a code change.
- admin-guard.mjs (+ tests): per-console host / IP-CIDR allowlists, letting the
  super-admin surface be locked down harder than moderation.

server.mjs no longer serves or mounts either surface. user-auth.mjs gains
pagination on listUsers/listUsageRecords/listBillingLedger to back the admin
dashboards. dev.mjs launches memind_adm; the ops SPA proxies /api to it while
/auth stays on the portal. Sessions are read from the shared cookie
(H5_COOKIE_DOMAIN=.tkmind.cn); login stays on the main domain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 20:55:23 +08:00

68 lines
2.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
parseList,
hostAllowed,
ipMatches,
ipAllowed,
buildNetworkGuard,
} from './admin-guard.mjs';
test('parseList splits, trims, and drops empties', () => {
assert.deepEqual(parseList('a, b ,,c'), ['a', 'b', 'c']);
assert.deepEqual(parseList(''), []);
assert.deepEqual(parseList(undefined), []);
});
test('hostAllowed: empty list allows all, otherwise matches hostname ignoring port/case', () => {
assert.equal(hostAllowed('gadm.tkmind.cn:8082', []), true);
assert.equal(hostAllowed('GADM.tkmind.cn', ['gadm.tkmind.cn']), true);
assert.equal(hostAllowed('evil.example.com', ['gadm.tkmind.cn']), false);
assert.equal(hostAllowed('gadm.tkmind.cn:443', ['gadm.tkmind.cn', 'localhost']), true);
});
test('ipMatches: exact and IPv4-mapped IPv6', () => {
assert.equal(ipMatches('127.0.0.1', '127.0.0.1'), true);
assert.equal(ipMatches('::ffff:127.0.0.1', '127.0.0.1'), true);
assert.equal(ipMatches('10.0.0.5', '10.0.0.6'), false);
});
test('ipMatches: CIDR ranges', () => {
assert.equal(ipMatches('10.1.2.3', '10.0.0.0/8'), true);
assert.equal(ipMatches('11.1.2.3', '10.0.0.0/8'), false);
assert.equal(ipMatches('192.168.1.50', '192.168.1.0/24'), true);
assert.equal(ipMatches('192.168.2.50', '192.168.1.0/24'), false);
assert.equal(ipMatches('203.0.113.9', '0.0.0.0/0'), true);
assert.equal(ipMatches('not-an-ip', '10.0.0.0/8'), false);
});
test('ipAllowed: empty list allows all', () => {
assert.equal(ipAllowed('1.2.3.4', []), true);
assert.equal(ipAllowed('1.2.3.4', ['10.0.0.0/8']), false);
assert.equal(ipAllowed('10.9.9.9', ['10.0.0.0/8', '127.0.0.1']), true);
});
test('buildNetworkGuard returns null when nothing configured', () => {
assert.equal(buildNetworkGuard({ onDeny: () => {} }), null);
});
test('buildNetworkGuard denies on host then ip, allows when both pass', () => {
const guard = buildNetworkGuard({
allowedHosts: ['gadm.tkmind.cn'],
ipAllowlist: ['10.0.0.0/8'],
onDeny: (_res, _req, reason) => `deny:${reason}`,
});
const mk = (host, ip) => ({ get: () => host, ip });
let nexted = false;
const next = () => {
nexted = true;
};
assert.equal(guard(mk('evil.com', '10.1.1.1'), {}, next), 'deny:host');
assert.equal(guard(mk('gadm.tkmind.cn', '8.8.8.8'), {}, next), 'deny:ip');
assert.equal(nexted, false);
guard(mk('gadm.tkmind.cn', '10.1.1.1'), {}, next);
assert.equal(nexted, true);
});