* Resolve an image annotation: fetch imageUrl → imageData if needed, * auto-detect dimensions, and set defaults for x/y. * * SECURITY: imageUrl is model-controlled. It must pass the same * validateUrl() gate as display_pdf/save_pdf — otherwise the model * can request `{image
(
ann: Record<string, any>,
)
| 1840 | * tool result carries the error; silent skip hides the attack attempt. |
| 1841 | */ |
| 1842 | async function resolveImageAnnotation( |
| 1843 | ann: Record<string, any>, |
| 1844 | ): Promise<void> { |
| 1845 | // Fetch image data from URL if no imageData provided |
| 1846 | if (!ann.imageData && ann.imageUrl) { |
| 1847 | const url = String(ann.imageUrl); |
| 1848 | // Same gate as every other local/remote read in this server. |
| 1849 | // Local: must be in allowedLocalFiles or under allowedLocalDirs. |
| 1850 | // Remote: must be https://. |
| 1851 | const check = validateUrl(url); |
| 1852 | if (!check.valid) { |
| 1853 | throw new Error( |
| 1854 | `imageUrl rejected by validateUrl: ${check.error ?? url}`, |
| 1855 | ); |
| 1856 | } |
| 1857 | let imgBytes: Uint8Array; |
| 1858 | if (url.startsWith("https://")) { |
| 1859 | const resp = await fetch(url); |
| 1860 | if (!resp.ok) throw new Error(`HTTP ${resp.status} for ${url}`); |
| 1861 | imgBytes = new Uint8Array(await resp.arrayBuffer()); |
| 1862 | } else { |
| 1863 | // validateUrl already confirmed this path is under an allowed root. |
| 1864 | const filePath = isFileUrl(url) |
| 1865 | ? fileUrlToPath(url) |
| 1866 | : decodeURIComponent(url); |
| 1867 | imgBytes = await fs.promises.readFile(path.resolve(filePath)); |
| 1868 | } |
| 1869 | ann.imageData = Buffer.from(imgBytes).toString("base64"); |
| 1870 | } |
| 1871 | |
| 1872 | // Auto-detect mimeType from magic bytes if not set |
| 1873 | if (ann.imageData && !ann.mimeType) { |
| 1874 | const bytes = Buffer.from(ann.imageData, "base64"); |
| 1875 | if ( |
| 1876 | bytes[0] === 0x89 && |
| 1877 | bytes[1] === 0x50 && |
| 1878 | bytes[2] === 0x4e && |
| 1879 | bytes[3] === 0x47 |
| 1880 | ) { |
| 1881 | ann.mimeType = "image/png"; |
| 1882 | } else { |
| 1883 | ann.mimeType = "image/jpeg"; |
| 1884 | } |
| 1885 | } |
| 1886 | |
| 1887 | // Auto-detect dimensions from image if not specified |
| 1888 | if (ann.imageData && (ann.width == null || ann.height == null)) { |
| 1889 | const dims = detectImageDimensions( |
| 1890 | Buffer.from(ann.imageData, "base64"), |
| 1891 | ); |
| 1892 | if (dims) { |
| 1893 | const maxWidth = 200; // default max width in PDF points |
| 1894 | const aspectRatio = dims.height / dims.width; |
| 1895 | ann.width = ann.width ?? Math.min(dims.width, maxWidth); |
| 1896 | ann.height = ann.height ?? ann.width * aspectRatio; |
| 1897 | } else { |
| 1898 | ann.width = ann.width ?? 200; |
| 1899 | ann.height = ann.height ?? 200; |
no test coverage detected
searching dependent graphs…