UNPKG

@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
"use strict"; 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