@lock-dev/geo-block
Version:
Geographic blocking module for lock.dev security framework
340 lines (330 loc) • 11.2 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/providers/ip-api.ts
var ip_api_exports = {};
__export(ip_api_exports, {
IpApiProvider: () => IpApiProvider
});
var IpApiProvider;
var init_ip_api = __esm({
"src/providers/ip-api.ts"() {
"use strict";
IpApiProvider = class {
constructor(config) {
this.apiKey = config.apiKey;
}
async init() {
}
async lookup(ip) {
try {
const url = this.apiKey ? `https://pro.ip-api.com/json/${ip}?key=${this.apiKey}&fields=status,message,countryCode,region,city,lat,lon` : `http://ip-api.com/json/${ip}?fields=status,message,countryCode,region,city,lat,lon`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`IP-API request failed with status: ${response.status}`);
}
const data = await response.json();
if (data.status === "success") {
return {
country: data.countryCode,
region: data.region,
city: data.city,
latitude: data.lat,
longitude: data.lon
};
} else {
console.warn(`IP-API lookup failed: ${data.message}`);
return {};
}
} catch (error) {
console.error(`Error looking up IP ${ip} with IP-API:`, error);
return {};
}
}
};
}
});
// src/index.ts
var index_exports = {};
__export(index_exports, {
GeoBlockEventType: () => GeoBlockEventType,
extractIp: () => extractIp,
geoBlock: () => geoBlock
});
module.exports = __toCommonJS(index_exports);
var import_lru_cache = require("lru-cache");
// src/types.ts
var GeoBlockEventType = /* @__PURE__ */ ((GeoBlockEventType2) => {
GeoBlockEventType2["GEO_BLOCKED"] = "geo_blocked";
return GeoBlockEventType2;
})(GeoBlockEventType || {});
// src/index.ts
var import_core = require("@lock-dev/core");
// src/providers/index.ts
var fs2 = __toESM(require("fs"), 1);
// src/providers/maxmind.ts
var maxmind = __toESM(require("maxmind"), 1);
var fs = __toESM(require("fs"), 1);
var MaxMindProvider = class {
/**
* Create a new MaxMind provider
* @param config Configuration options
*/
constructor(config) {
this.initialized = false;
if (!config.maxmindDbPath) {
throw new Error("MaxMind database path must be provided");
}
this.dbPath = config.maxmindDbPath;
}
/**
* Initialize the MaxMind database reader
*/
async init() {
if (this.initialized) return;
try {
if (!fs.existsSync(this.dbPath)) {
throw new Error(`MaxMind database file not found at: ${this.dbPath}`);
}
this.reader = await maxmind.open(this.dbPath);
this.initialized = true;
} catch (error) {
throw new Error(
`Failed to initialize MaxMind database: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Look up geographic information for an IP address
* @param ip IP address to look up
*/
async lookup(ip) {
if (!this.initialized) {
await this.init();
}
try {
const result = this.reader.get(ip);
if (!result) {
return {};
}
return {
country: result.country?.iso_code,
region: result.subdivisions?.[0]?.iso_code,
city: result.city?.names?.en,
latitude: result.location?.latitude,
longitude: result.location?.longitude
};
} catch (error) {
console.error(`Error looking up IP ${ip}:`, error);
return {};
}
}
};
// src/providers/index.ts
init_ip_api();
function createProvider(config) {
if (config.provider === "maxmind") {
if (!config.maxmindDbPath || !fs2.existsSync(config.maxmindDbPath)) {
console.warn(
`MaxMind database not found at path: ${config.maxmindDbPath || "undefined"}. Falling back to ip-api.com service.`
);
return new IpApiProvider(config);
}
return new MaxMindProvider(config);
}
switch (config.provider) {
case "ipapi":
return new IpApiProvider(config);
case "custom":
if (!config.customLookup) {
throw new Error("Custom lookup function must be provided when using custom provider");
}
return {
init: async () => {
},
lookup: config.customLookup
};
default:
console.warn(`Unknown provider: ${config.provider}. Falling back to ip-api.com service.`);
return new IpApiProvider(config);
}
}
// src/utils/extract-ip.ts
function extractIp(request, ipHeaders = [], useRemoteAddress = true) {
if (request.headers) {
for (const header of ipHeaders) {
const value = request.headers[header.toLowerCase()];
if (value) {
const parts = value.split(",");
return parts[0].trim();
}
}
}
if (useRemoteAddress) {
if (request.connection && request.connection.remoteAddress) {
return request.connection.remoteAddress;
}
if (request.socket && request.socket.remoteAddress) {
return request.socket.remoteAddress;
}
}
return null;
}
// src/index.ts
var DEFAULT_CONFIG = {
mode: "blocklist",
ipHeaders: ["cf-connecting-ip", "x-forwarded-for", "x-real-ip"],
useRemoteAddress: true,
blockStatusCode: 403,
blockMessage: "Access denied based on your location",
includeCountryInResponse: true,
provider: "maxmind",
cacheTtl: 36e5,
// 1 hour
cacheSize: 1e4,
failBehavior: "open"
};
var geoBlock = (0, import_core.createModule)({
name: "geo-block",
defaultConfig: DEFAULT_CONFIG,
async check(context, config) {
try {
let provider = createProvider(config);
try {
await provider.init();
} catch (initError) {
console.error(`Failed to initialize geo provider: ${initError.message}`);
if (config.failBehavior === "closed") {
return {
passed: false,
reason: "geo_blocked" /* GEO_BLOCKED */,
data: { error: "Geo provider failed to initialize" },
severity: "medium"
};
}
if (config.provider !== "ipapi") {
try {
const IpApiProvider2 = (init_ip_api(), __toCommonJS(ip_api_exports)).IpApiProvider;
const fallbackProvider = new IpApiProvider2(config);
await fallbackProvider.init();
provider = fallbackProvider;
console.warn("Successfully switched to ip-api fallback provider");
} catch (fallbackError) {
console.error(`Fallback provider also failed: ${fallbackError.message}`);
return { passed: true };
}
} else {
return { passed: true };
}
}
const cache = new import_lru_cache.LRUCache({
max: config.cacheSize,
ttl: config.cacheTtl,
ttlAutopurge: true
});
const ip = extractIp(context.request, config.ipHeaders, config.useRemoteAddress);
if (!ip) {
console.warn("No IP address could be extracted from the request");
return { passed: true };
}
try {
let geoInfo = cache.get(ip);
if (!geoInfo) {
geoInfo = await provider.lookup(ip);
if (geoInfo && Object.keys(geoInfo).length > 0) {
cache.set(ip, geoInfo);
}
}
if (!geoInfo || !geoInfo.country) {
console.warn(`No country information found for IP: ${ip}`);
return { passed: true };
}
const country = geoInfo.country;
const isBlocked = config.mode === "blocklist" && config.countries.includes(country) || config.mode === "allowlist" && !config.countries.includes(country);
if (isBlocked) {
return {
passed: false,
reason: "geo_blocked" /* GEO_BLOCKED */,
data: { ip, country, geoInfo },
severity: "medium"
};
}
return { passed: true };
} catch (lookupError) {
console.error(`Error during geo lookup for IP ${ip}:`, lookupError);
if (config.failBehavior === "closed") {
return {
passed: false,
reason: "geo_blocked" /* GEO_BLOCKED */,
data: { error: "Geo lookup failed", ip },
severity: "medium"
};
}
return { passed: true };
}
} catch (error) {
console.error(`Unexpected error in geo-block module:`, error);
return {
passed: config.failBehavior !== "closed",
reason: config.failBehavior === "closed" ? "geo_blocked" /* GEO_BLOCKED */ : void 0,
data: config.failBehavior === "closed" ? { error: "Geo-block module failed" } : void 0,
severity: "medium"
};
}
},
async handleFailure(context, reason, data) {
const config = context.data.get("geo-block:config");
const res = context.response;
if (typeof res.status === "function" || typeof res.statusCode === "number") {
if (typeof res.status === "function") {
res.status(config.blockStatusCode ?? 403).json({
error: config.blockMessage ?? "Access denied based on your location",
...config.includeCountryInResponse && data?.country ? { country: data.country } : {}
});
} else {
res.statusCode = config.blockStatusCode ?? 403;
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
error: config.blockMessage ?? "Access denied based on your location",
...config.includeCountryInResponse && data?.country ? { country: data.country } : {}
})
);
}
}
}
});
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
GeoBlockEventType,
extractIp,
geoBlock
});