@ankimcp/anki-mcp-server
Version:
Model Context Protocol server for Anki - enables AI assistants to interact with your Anki flashcards
228 lines • 8.59 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MediaUrlInvalidError = exports.MediaUrlSchemeError = exports.MediaUrlBlockedError = exports.MediaImportDirError = exports.MediaFileTypeError = void 0;
exports.validateMediaFilePath = validateMediaFilePath;
exports.validateMediaUrl = validateMediaUrl;
exports.sanitizeMediaFilename = sanitizeMediaFilename;
exports.getMediaFilePathConfigFromEnv = getMediaFilePathConfigFromEnv;
exports.getMediaUrlConfigFromEnv = getMediaUrlConfigFromEnv;
const path = __importStar(require("node:path"));
const dns = __importStar(require("node:dns"));
const mime_1 = __importDefault(require("mime"));
const ipaddr = __importStar(require("ipaddr.js"));
class MediaFileTypeError extends Error {
constructor() {
super("File type not allowed. Only media files (images, audio, video) are accepted. " +
"To allow additional file types, set the MEDIA_ALLOWED_TYPES environment variable.");
this.name = "MediaFileTypeError";
}
}
exports.MediaFileTypeError = MediaFileTypeError;
class MediaImportDirError extends Error {
constructor(configuredDir) {
super(`File path is outside the allowed import directory (${configuredDir}). ` +
"Update MEDIA_IMPORT_DIR to change the allowed directory.");
this.name = "MediaImportDirError";
}
}
exports.MediaImportDirError = MediaImportDirError;
class MediaUrlBlockedError extends Error {
constructor() {
super("URL blocked: requests to private/internal networks are not allowed. " +
"To allow specific hosts, set the MEDIA_ALLOWED_HOSTS environment variable.");
this.name = "MediaUrlBlockedError";
}
}
exports.MediaUrlBlockedError = MediaUrlBlockedError;
class MediaUrlSchemeError extends Error {
constructor(scheme) {
super(`URL scheme "${scheme}" is not allowed. Only http: and https: URLs are accepted.`);
this.name = "MediaUrlSchemeError";
}
}
exports.MediaUrlSchemeError = MediaUrlSchemeError;
class MediaUrlInvalidError extends Error {
constructor() {
super("Invalid URL provided.");
this.name = "MediaUrlInvalidError";
}
}
exports.MediaUrlInvalidError = MediaUrlInvalidError;
const DEFAULT_ALLOWED_PREFIXES = ["image/", "audio/", "video/"];
const ALLOWED_RANGE = "unicast";
function isPublicUnicast(addr) {
return addr.range() === ALLOWED_RANGE;
}
function isIPv4CompatibleAddress(ipv6) {
return ipv6.parts.slice(0, 6).every((part) => part === 0);
}
function extractEmbeddedIPv4(ipv6) {
const high = ipv6.parts[6];
const low = ipv6.parts[7];
return new ipaddr.IPv4([high >> 8, high & 0xff, low >> 8, low & 0xff]);
}
function parseTargetAddress(ip) {
if (!ipaddr.isValid(ip))
return null;
let addr = ipaddr.parse(ip);
if (addr instanceof ipaddr.IPv6) {
if (addr.isIPv4MappedAddress())
addr = addr.toIPv4Address();
else if (isIPv4CompatibleAddress(addr))
addr = extractEmbeddedIPv4(addr);
}
return addr;
}
function assertAddressAllowed(ip) {
const addr = parseTargetAddress(ip);
if (!addr || !isPublicUnicast(addr)) {
throw new MediaUrlBlockedError();
}
}
function normalizeIp(value) {
return parseTargetAddress(value)?.toString() ?? null;
}
function validateMediaFilePath(filePath, config = {}) {
if (filePath.includes("\0")) {
throw new MediaFileTypeError();
}
const resolvedPath = path.resolve(filePath);
const detectedMime = mime_1.default.getType(resolvedPath);
const isDefaultAllowed = detectedMime !== null &&
DEFAULT_ALLOWED_PREFIXES.some((prefix) => detectedMime.startsWith(prefix));
const isExtraAllowed = detectedMime !== null &&
config.allowedTypes !== undefined &&
config.allowedTypes.includes(detectedMime);
if (!isDefaultAllowed && !isExtraAllowed) {
throw new MediaFileTypeError();
}
if (config.importDir) {
const resolvedImportDir = path.resolve(config.importDir);
const normalizedImportDir = resolvedImportDir.endsWith(path.sep)
? resolvedImportDir
: resolvedImportDir + path.sep;
if (!resolvedPath.startsWith(normalizedImportDir)) {
throw new MediaImportDirError(config.importDir);
}
}
return { resolvedPath, mimeType: detectedMime };
}
async function validateMediaUrl(input, config = {}) {
let parsed;
try {
parsed = new URL(input);
}
catch {
throw new MediaUrlInvalidError();
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new MediaUrlSchemeError(parsed.protocol.replace(":", ""));
}
const hostname = parsed.hostname;
const hostnameForLookup = hostname.startsWith("[") && hostname.endsWith("]")
? hostname.slice(1, -1)
: hostname;
let resolved;
try {
resolved = await dns.promises.lookup(hostnameForLookup, { all: true });
}
catch {
throw new MediaUrlInvalidError();
}
if (resolved.length === 0) {
throw new MediaUrlInvalidError();
}
const resolvedIp = resolved[0].address;
if (config.allowedHosts && config.allowedHosts.length > 0) {
const allowedHosts = config.allowedHosts;
const hostnameAllowed = allowedHosts.includes(hostname) ||
allowedHosts.includes(hostnameForLookup);
const allowedIps = allowedHosts
.map(normalizeIp)
.filter((ip) => ip !== null);
const allIpsAllowed = resolved.every(({ address }) => {
const normalized = normalizeIp(address);
return normalized !== null && allowedIps.includes(normalized);
});
if (hostnameAllowed || allIpsAllowed) {
return { hostname, resolvedIp };
}
}
for (const { address } of resolved) {
assertAddressAllowed(address);
}
return { hostname, resolvedIp };
}
function sanitizeMediaFilename(filename) {
let sanitized = filename.replace(/\0/g, "");
sanitized = sanitized.replace(/\.\./g, "");
sanitized = sanitized.replace(/[/\\]/g, "");
sanitized = path.basename(sanitized);
if (!sanitized || sanitized.trim() === "" || sanitized === ".") {
return "unnamed";
}
return sanitized;
}
function getMediaFilePathConfigFromEnv() {
const config = {};
const allowedTypes = process.env.MEDIA_ALLOWED_TYPES;
if (allowedTypes) {
config.allowedTypes = allowedTypes
.split(",")
.map((t) => t.trim())
.filter(Boolean);
}
const importDir = process.env.MEDIA_IMPORT_DIR;
if (importDir) {
config.importDir = importDir;
}
return config;
}
function getMediaUrlConfigFromEnv() {
const config = {};
const allowedHosts = process.env.MEDIA_ALLOWED_HOSTS;
if (allowedHosts) {
config.allowedHosts = allowedHosts
.split(",")
.map((h) => h.trim())
.filter(Boolean);
}
return config;
}
//# sourceMappingURL=media-validation.utils.js.map