Files
memind/admin-guard.mjs
Your Name b0f5d6a51c Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 16:39:39 -07:00

79 lines
2.7 KiB
JavaScript

// admin-guard.mjs
//
// Pure, testable network-guard helpers for the memind_adm consoles.
//
// Each back-office console (platform super-admin, plaza operations) can be
// pinned to an allowlist of request hosts and/or client IPs, independently.
// This lets the super-admin surface be locked down harder than moderation
// while both still live in one process — and the same knobs keep working
// unchanged once the consoles are split into separate processes.
export function parseList(value) {
return String(value ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean);
}
export function hostAllowed(reqHost, allowedHosts) {
if (!allowedHosts.length) return true;
const hostname = String(reqHost ?? '').split(':')[0].toLowerCase();
return allowedHosts.some((h) => h.toLowerCase() === hostname);
}
function ipv4ToInt(ip) {
const parts = ip.split('.');
if (parts.length !== 4) return null;
let acc = 0;
for (const part of parts) {
if (!/^\d{1,3}$/.test(part)) return null;
const n = Number(part);
if (n < 0 || n > 255) return null;
acc = (acc * 256 + n) >>> 0;
}
return acc >>> 0;
}
// Strip an IPv4-mapped IPv6 prefix so "::ffff:127.0.0.1" matches "127.0.0.1".
function normalizeIp(ip) {
return String(ip ?? '').replace(/^::ffff:/i, '');
}
export function ipMatches(clientIp, rule) {
const ip = normalizeIp(clientIp);
if (rule.includes('/')) {
const [base, bitsStr] = rule.split('/');
const bits = Number(bitsStr);
const ipInt = ipv4ToInt(ip);
const baseInt = ipv4ToInt(base);
if (ipInt === null || baseInt === null) return false;
if (!Number.isInteger(bits) || bits < 0 || bits > 32) return false;
if (bits === 0) return true;
const mask = bits === 32 ? 0xffffffff : (~((1 << (32 - bits)) - 1)) >>> 0;
return (ipInt & mask) === (baseInt & mask);
}
return ip === normalizeIp(rule);
}
export function ipAllowed(clientIp, ipAllowlist) {
if (!ipAllowlist.length) return true;
return ipAllowlist.some((rule) => ipMatches(clientIp, rule));
}
/**
* Build an Express middleware enforcing host + IP allowlists for one console.
* Returns null when neither list is configured (no-op — common in dev).
* @param {object} opts
* @param {string[]} [opts.allowedHosts]
* @param {string[]} [opts.ipAllowlist]
* @param {(res, req, reason: 'host'|'ip') => unknown} opts.onDeny
*/
export function buildNetworkGuard({ allowedHosts = [], ipAllowlist = [], onDeny }) {
if (!allowedHosts.length && !ipAllowlist.length) return null;
return (req, res, next) => {
if (!hostAllowed(req.get('host'), allowedHosts)) return onDeny(res, req, 'host');
if (!ipAllowed(req.ip, ipAllowlist)) return onDeny(res, req, 'ip');
return next();
};
}