agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
93 lines • 3.91 kB
JavaScript
;
/**
* hosting/signin/clientAddress — which client a request came from, for the
* per-address attempt budget.
*
* The socket's address, unless the peer is a TRUSTED proxy: then the rightmost
* `X-Forwarded-For` hop that is not itself trusted (a client can prepend any
* hops it likes; only the ones our own proxies appended are believed).
* `X-Forwarded-For` from anybody else is ignored.
*
* `trustedProxies` entries are IP literals or CIDR ranges (`10.0.0.0/8`,
* `fd00::/8`), matched as ranges — anything else is refused at construction,
* so a typo cannot quietly match nothing.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.clientAddress = exports.trustedProxies = void 0;
const node_net_1 = require("node:net");
const errors_js_1 = require("./errors.js");
/** Parse `trustedProxies`, or refuse the first entry that is not an IP or a CIDR range. */
function trustedProxies(entries) {
const list = new node_net_1.BlockList();
for (const raw of entries ?? []) {
const entry = typeof raw === 'string' ? raw.trim() : '';
const [ip, bits, extra] = entry.split('/');
const family = (0, node_net_1.isIP)(ip ?? '');
const prefix = bits === undefined ? undefined : Number(bits);
const max = family === 6 ? 128 : 32;
if (family === 0 ||
extra !== undefined ||
(prefix !== undefined &&
(!Number.isInteger(prefix) || prefix < 0 || prefix > max || bits === ''))) {
throw new errors_js_1.SignInDoorConfigError('trustedProxies', `trustedProxies entry ${JSON.stringify(raw)} is not an IP address or a CIDR range ` +
`(e.g. 10.0.0.5, 10.0.0.0/8, fd00::/8)`);
}
const type = family === 6 ? 'ipv6' : 'ipv4';
if (prefix === undefined)
list.addAddress(ip, type);
else
list.addSubnet(ip, prefix, type);
}
const configured = (entries ?? []).length > 0;
return {
configured,
has(address) {
const family = (0, node_net_1.isIP)(address);
if (family === 0)
return false;
return list.check(address, family === 6 ? 'ipv6' : 'ipv4');
},
};
}
exports.trustedProxies = trustedProxies;
/**
* The client address. `forwardedUnset` is called when a request carries
* `X-Forwarded-For` and no proxy is trusted — the door warns once that every
* person behind that proxy then shares one address budget.
*/
function clientAddress(req, trusted, forwardedUnset) {
const peer = normaliseAddress(req.socket?.remoteAddress ?? 'unknown');
const header = req.headers['x-forwarded-for'];
if (!trusted.has(peer)) {
if (header !== undefined && !trusted.configured)
forwardedUnset?.();
return peer;
}
const hops = (Array.isArray(header) ? header.join(',') : header ?? '')
.split(',')
.map((h) => normaliseAddress(withoutPort(h.trim())))
.filter((h) => h.length > 0);
for (let i = hops.length - 1; i >= 0; i -= 1) {
const hop = hops[i];
if (!trusted.has(hop))
return hop;
}
return peer;
}
exports.clientAddress = clientAddress;
/**
* A forwarded hop without its port: some proxies send `1.2.3.4:5678` or
* `[2001:db8::1]:5678`, and a port per source connection would make every
* attempt a new address, so the delay never grew (review idI57 N-11).
*/
function withoutPort(hop) {
const bracketed = /^\[([^\]]+)\](?::\d{1,5})?$/.exec(hop);
if (bracketed !== null)
return bracketed[1];
const v4 = /^(\d{1,3}(?:\.\d{1,3}){3}):\d{1,5}$/.exec(hop);
return v4 !== null ? v4[1] : hop;
}
function normaliseAddress(address) {
return address.startsWith('::ffff:') && (0, node_net_1.isIP)(address.slice(7)) === 4 ? address.slice(7) : address;
}
//# sourceMappingURL=clientAddress.js.map