@juspay/neurolink 10.10.12 → 10.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/adapters/imageFormatSupport.d.ts +75 -0
  3. package/dist/adapters/imageFormatSupport.js +283 -0
  4. package/dist/adapters/video/ffmpegAdapter.d.ts +6 -0
  5. package/dist/adapters/video/ffmpegAdapter.js +1 -1
  6. package/dist/browser/neurolink.min.js +399 -398
  7. package/dist/lib/adapters/imageFormatSupport.d.ts +75 -0
  8. package/dist/lib/adapters/imageFormatSupport.js +284 -0
  9. package/dist/lib/adapters/video/ffmpegAdapter.d.ts +6 -0
  10. package/dist/lib/adapters/video/ffmpegAdapter.js +1 -1
  11. package/dist/lib/processors/config/fileExtensions.d.ts +32 -15
  12. package/dist/lib/processors/config/fileExtensions.js +27 -66
  13. package/dist/lib/processors/config/fileTypeRegistry.d.ts +106 -0
  14. package/dist/lib/processors/config/fileTypeRegistry.js +702 -0
  15. package/dist/lib/processors/config/index.d.ts +2 -1
  16. package/dist/lib/processors/config/index.js +5 -1
  17. package/dist/lib/processors/config/mimeConstants.d.ts +22 -7
  18. package/dist/lib/processors/config/mimeConstants.js +45 -66
  19. package/dist/lib/processors/media/AudioProcessor.js +16 -38
  20. package/dist/lib/processors/media/VideoProcessor.js +11 -32
  21. package/dist/lib/providers/googleAiStudio/client.js +12 -1
  22. package/dist/lib/providers/googleVertex/client.js +123 -66
  23. package/dist/lib/types/file.d.ts +41 -0
  24. package/dist/lib/utils/fileDetector.js +367 -251
  25. package/dist/lib/utils/imageProcessor.js +14 -17
  26. package/dist/lib/utils/markupSniff.d.ts +37 -0
  27. package/dist/lib/utils/markupSniff.js +125 -0
  28. package/dist/lib/utils/messageBuilder.d.ts +14 -0
  29. package/dist/lib/utils/messageBuilder.js +172 -92
  30. package/dist/processors/config/fileExtensions.d.ts +32 -15
  31. package/dist/processors/config/fileExtensions.js +27 -66
  32. package/dist/processors/config/fileTypeRegistry.d.ts +106 -0
  33. package/dist/processors/config/fileTypeRegistry.js +701 -0
  34. package/dist/processors/config/index.d.ts +2 -1
  35. package/dist/processors/config/index.js +5 -1
  36. package/dist/processors/config/mimeConstants.d.ts +22 -7
  37. package/dist/processors/config/mimeConstants.js +45 -66
  38. package/dist/processors/media/AudioProcessor.js +16 -38
  39. package/dist/processors/media/VideoProcessor.js +11 -32
  40. package/dist/providers/googleAiStudio/client.js +12 -1
  41. package/dist/providers/googleVertex/client.js +123 -66
  42. package/dist/types/file.d.ts +41 -0
  43. package/dist/utils/fileDetector.js +367 -251
  44. package/dist/utils/imageProcessor.js +14 -17
  45. package/dist/utils/markupSniff.d.ts +37 -0
  46. package/dist/utils/markupSniff.js +124 -0
  47. package/dist/utils/messageBuilder.d.ts +14 -0
  48. package/dist/utils/messageBuilder.js +172 -92
  49. package/package.json +3 -2
@@ -21,10 +21,15 @@ async function getArchiveProcessor() {
21
21
  return mod.archiveProcessor;
22
22
  }
23
23
  import { tracers, ATTR, withSpan } from "../telemetry/index.js";
24
+ import { CONFIG_EXTENSIONS } from "../processors/config/fileExtensions.js";
25
+ import { fileTypeForExtension, lookupByMimeType, normalizeExtension, } from "../processors/config/fileTypeRegistry.js";
26
+ import { LANGUAGE_MAP } from "../processors/config/languageMap.js";
27
+ import { getMimeTypeForExtension, TEXT_EXTENSION_MIME_MAP, } from "../processors/config/mimeConstants.js";
24
28
  import { CSVProcessor } from "./csvProcessor.js";
25
29
  import { ImageProcessor } from "./imageProcessor.js";
26
30
  import { detectIsoBmffImageMimeType, hasFtypBoxSignature } from "./isoBmff.js";
27
31
  import { logger } from "./logger.js";
32
+ import { looksLikeSvgMarkup } from "./markupSniff.js";
28
33
  import { withTimeout } from "./errorHandling.js";
29
34
  import { normalizeUrlForCache, redactUrlForError, sanitizeErrorCause, } from "./logSanitize.js";
30
35
  import { mimeHintToExtension, mimeHintToFileType, normalizeMimeHint, } from "./mimeTypeHints.js";
@@ -1642,15 +1647,164 @@ export class FileDetector {
1642
1647
  return Buffer.from(match[2], "base64");
1643
1648
  }
1644
1649
  }
1650
+ /**
1651
+ * Resolve an extension to a routing {@link FileType}.
1652
+ *
1653
+ * Consults, in order:
1654
+ * 1. the canonical file-type registry — every image / audio / video /
1655
+ * document / data / archive format;
1656
+ * 2. the text, markup and config MIME map;
1657
+ * 3. `LANGUAGE_MAP`, which already enumerates ~200 source-code extensions.
1658
+ *
1659
+ * (3) is why this is a function rather than a table: source code was
1660
+ * previously a third hand-written list inside the detector covering roughly a
1661
+ * quarter of the languages the code processor already knew about, so a `.zig`
1662
+ * or `.erl` file was "unknown" and fell through to content heuristics.
1663
+ *
1664
+ * Returns undefined when the extension is not recognised at all.
1665
+ */
1666
+ function resolveFileTypeForExtension(ext) {
1667
+ const registryType = fileTypeForExtension(ext);
1668
+ if (registryType) {
1669
+ return registryType;
1670
+ }
1671
+ const normalized = normalizeExtension(ext);
1672
+ if (TEXT_EXTENSION_MIME_MAP[normalized] ||
1673
+ LANGUAGE_MAP[normalized] ||
1674
+ CONFIG_EXTENSIONS.includes(normalized)) {
1675
+ return "text";
1676
+ }
1677
+ return undefined;
1678
+ }
1679
+ /**
1680
+ * Bytes read from the head of a file when running magic-byte detection against
1681
+ * a path.
1682
+ *
1683
+ * 4 KB rather than a few dozen: the largest consumers are the M2TS sync check
1684
+ * (three 192-byte packets), the ASF stream-type GUID (which sits past the
1685
+ * header object), and the OOXML sniff, which looks for `xl/`, `word/` or
1686
+ * `ppt/` entry names inside a ZIP directory. Still a single small read, and
1687
+ * far cheaper than the whole-file read the content heuristics would otherwise
1688
+ * perform.
1689
+ */
1690
+ const MAGIC_BYTES_HEADER_SIZE = 4096;
1691
+ /**
1692
+ * ASF stream-type GUIDs, little-endian as stored in the file.
1693
+ *
1694
+ * `.wmv` and `.wma` share the same container signature, so the container alone
1695
+ * cannot say which modality a file is. These GUIDs appear in the stream
1696
+ * properties object and do.
1697
+ */
1698
+ const ASF_VIDEO_MEDIA_GUID = Buffer.from("c0ef19bc4d5bcf11a8fd00805f5c442b", "hex");
1699
+ const ASF_AUDIO_MEDIA_GUID = Buffer.from("409e69f84d5bcf11a8fd00805f5c442b", "hex");
1700
+ /**
1701
+ * Identify an OOXML or OpenDocument package inside a ZIP.
1702
+ *
1703
+ * Every Office format is a ZIP, so the ZIP signature alone routes .xlsx, .docx,
1704
+ * .pptx and .odt to the archive processor whenever no filename is available —
1705
+ * which is exactly the shape of a Slack or API upload that arrives as a bare
1706
+ * Buffer. Reading the entry names recovers the real type.
1707
+ *
1708
+ * ODF is exact: the spec requires a `mimetype` entry stored first and
1709
+ * uncompressed, so the media type is literally in the bytes. OOXML has no such
1710
+ * guarantee, so its part prefixes (`xl/`, `word/`, `ppt/`) are matched instead,
1711
+ * gated on `[Content_Types].xml` being present so a plain ZIP that happens to
1712
+ * contain a folder called `word/` is not misread.
1713
+ *
1714
+ * Returns null when the ZIP is inconclusive, leaving the archive fallback and
1715
+ * the extension to decide.
1716
+ */
1717
+ function detectZipPackageType(input) {
1718
+ const head = input.toString("latin1", 0, Math.min(input.length, 4096));
1719
+ // ODF: the mimetype string follows the stored "mimetype" entry name directly.
1720
+ const odfMatch = head.match(/mimetype(application\/vnd\.oasis\.opendocument\.[a-z-]+)/);
1721
+ if (odfMatch) {
1722
+ const mimeType = odfMatch[1];
1723
+ const entry = lookupByMimeType(mimeType);
1724
+ if (entry) {
1725
+ return { type: entry.fileType, mimeType };
1726
+ }
1727
+ }
1728
+ if (!head.includes("[Content_Types].xml")) {
1729
+ return null;
1730
+ }
1731
+ for (const [prefix, mimeType] of [
1732
+ [
1733
+ "xl/",
1734
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1735
+ ],
1736
+ [
1737
+ "word/",
1738
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1739
+ ],
1740
+ [
1741
+ "ppt/",
1742
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1743
+ ],
1744
+ ]) {
1745
+ if (head.includes(prefix)) {
1746
+ const entry = lookupByMimeType(mimeType);
1747
+ if (entry) {
1748
+ return { type: entry.fileType, mimeType };
1749
+ }
1750
+ }
1751
+ }
1752
+ return null;
1753
+ }
1645
1754
  /**
1646
1755
  * Strategy 1: Magic Bytes Detection (95% confidence)
1647
1756
  * Detects file type from binary file headers
1648
1757
  */
1649
1758
  class MagicBytesStrategy {
1650
1759
  async detect(input) {
1651
- if (!Buffer.isBuffer(input)) {
1760
+ const buffer = await MagicBytesStrategy.resolveBuffer(input);
1761
+ if (!buffer) {
1652
1762
  return this.unknown();
1653
1763
  }
1764
+ return this.detectFromBuffer(buffer);
1765
+ }
1766
+ /**
1767
+ * Obtain the header bytes to inspect.
1768
+ *
1769
+ * Buffers are used directly. For a filesystem path we read only the first
1770
+ * {@link MAGIC_BYTES_HEADER_SIZE} bytes — this strategy previously bailed out
1771
+ * for anything that was not already a Buffer, which meant a path was
1772
+ * classified by its extension alone and any file whose extension was missing,
1773
+ * wrong or ambiguous fell through to the content heuristics. Those heuristics
1774
+ * `readFile()` the *entire* file, so detecting a 500 MB video used to read
1775
+ * 500 MB into memory to conclude nothing; now it reads
1776
+ * {@link MAGIC_BYTES_HEADER_SIZE} bytes and returns.
1777
+ *
1778
+ * URLs and data URIs are left alone: a URL would need a network round-trip
1779
+ * (the MIME strategy handles it), and a data URI already carries its type.
1780
+ */
1781
+ static async resolveBuffer(input) {
1782
+ if (Buffer.isBuffer(input)) {
1783
+ return input;
1784
+ }
1785
+ if (typeof input !== "string" ||
1786
+ input.startsWith("data:") ||
1787
+ input.startsWith("http://") ||
1788
+ input.startsWith("https://")) {
1789
+ return undefined;
1790
+ }
1791
+ let handle;
1792
+ try {
1793
+ handle = await open(input, "r");
1794
+ const header = Buffer.alloc(MAGIC_BYTES_HEADER_SIZE);
1795
+ const { bytesRead } = await handle.read(header, 0, header.length, 0);
1796
+ return bytesRead > 0 ? header.subarray(0, bytesRead) : undefined;
1797
+ }
1798
+ catch {
1799
+ // Not a readable path (nonexistent, a directory, permission denied).
1800
+ // Detection continues with the remaining strategies.
1801
+ return undefined;
1802
+ }
1803
+ finally {
1804
+ await handle?.close().catch(() => undefined);
1805
+ }
1806
+ }
1807
+ detectFromBuffer(input) {
1654
1808
  if (this.isPNG(input)) {
1655
1809
  return this.result("image", "image/png", 95);
1656
1810
  }
@@ -1709,6 +1863,18 @@ class MagicBytesStrategy {
1709
1863
  if (brand.startsWith("qt")) {
1710
1864
  return this.result("video", "video/quicktime", 95);
1711
1865
  }
1866
+ // 3GPP / 3GPP2 share the ftyp box. Brands are "3gp4".."3gp9", "3gr6",
1867
+ // "3gs7", "3ge6" … for 3GPP and "3g2a".."3g2c" for 3GPP2. A 3GPP file can
1868
+ // hold audio only (phone voice memos) or audio+video, and the brand does
1869
+ // not say which — so this reports video at a confidence *below* the
1870
+ // detection threshold, letting a `.3gp`/`.3g2` extension confirm it and
1871
+ // an explicit audio mimetype hint override it.
1872
+ if (brand.startsWith("3g2")) {
1873
+ return this.result("video", "video/3gpp2", 75);
1874
+ }
1875
+ if (brand.startsWith("3g")) {
1876
+ return this.result("video", "video/3gpp", 75);
1877
+ }
1712
1878
  return this.result("video", "video/mp4", 95);
1713
1879
  }
1714
1880
  // EBML container (MKV/WebM) — both share the 0x1A45DFA3 header; the DocType
@@ -1748,6 +1914,104 @@ class MagicBytesStrategy {
1748
1914
  input[11] === 0x45) {
1749
1915
  return this.result("audio", "audio/wav", 95);
1750
1916
  }
1917
+ // AIFF / AIFF-C: "FORM" + "AIFF" or "AIFC" at offset 8. Same IFF container
1918
+ // shape as RIFF above, big-endian.
1919
+ if (input.length >= 12 &&
1920
+ input.toString("latin1", 0, 4) === "FORM" &&
1921
+ (input.toString("latin1", 8, 12) === "AIFF" ||
1922
+ input.toString("latin1", 8, 12) === "AIFC")) {
1923
+ return this.result("audio", "audio/aiff", 95);
1924
+ }
1925
+ // ASF container — Windows Media. The header GUID is shared by .wmv (video)
1926
+ // and .wma (audio), so the stream-type GUID inside decides. A .wma read
1927
+ // from a bare Buffer was reported as video before this lookup existed.
1928
+ if (input.length >= 4 &&
1929
+ input[0] === 0x30 &&
1930
+ input[1] === 0x26 &&
1931
+ input[2] === 0xb2 &&
1932
+ input[3] === 0x75) {
1933
+ if (input.includes(ASF_VIDEO_MEDIA_GUID)) {
1934
+ return this.result("video", "video/x-ms-wmv", 92);
1935
+ }
1936
+ if (input.includes(ASF_AUDIO_MEDIA_GUID)) {
1937
+ return this.result("audio", "audio/x-ms-wma", 92);
1938
+ }
1939
+ // Neither GUID within the header slice — report below the detection
1940
+ // threshold so an extension, if there is one, still wins.
1941
+ return this.result("video", "video/x-ms-asf", 75);
1942
+ }
1943
+ // FLV: "FLV" + version byte.
1944
+ if (input.length >= 4 &&
1945
+ input.toString("latin1", 0, 3) === "FLV" &&
1946
+ input[3] === 0x01) {
1947
+ return this.result("video", "video/x-flv", 95);
1948
+ }
1949
+ // Rich Text Format: "{\rtf".
1950
+ if (input.length >= 5 && input.toString("latin1", 0, 5) === "{\\rtf") {
1951
+ return this.result("docx", "application/rtf", 92);
1952
+ }
1953
+ // Core Audio Format: "caff".
1954
+ if (input.length >= 4 && input.toString("latin1", 0, 4) === "caff") {
1955
+ return this.result("audio", "audio/x-caf", 95);
1956
+ }
1957
+ // Sun/NeXT audio: ".snd".
1958
+ if (input.length >= 4 && input.toString("latin1", 0, 4) === ".snd") {
1959
+ return this.result("audio", "audio/basic", 95);
1960
+ }
1961
+ // MPEG program stream (0x000001BA) and elementary video stream (0x000001B3)
1962
+ // — .mpg/.mpeg/.vob. Without this, an MPEG-1 file reached the content
1963
+ // heuristics, whose CSV check found consistent delimiter counts in the
1964
+ // binary and classified real video as a spreadsheet.
1965
+ if (input.length >= 4 &&
1966
+ input[0] === 0x00 &&
1967
+ input[1] === 0x00 &&
1968
+ input[2] === 0x01 &&
1969
+ (input[3] === 0xba || input[3] === 0xb3)) {
1970
+ return this.result("video", "video/mpeg", 95);
1971
+ }
1972
+ // MPEG-2 transport stream — .ts/.mts/.m2ts. There is no magic number, only
1973
+ // a 0x47 sync byte at a fixed stride; requiring three in a row keeps a
1974
+ // stray 0x47 from claiming an unrelated file. This is also what rescues
1975
+ // `.ts`, whose extension resolves to TypeScript source.
1976
+ //
1977
+ // Two strides, because BDAV/M2TS prefixes each packet with a 4-byte arrival
1978
+ // timestamp: plain TS is 188 bytes from offset 0, M2TS is 192 from offset 4.
1979
+ // Checking only the former left every .m2ts undetected from a Buffer.
1980
+ if ((input.length >= 377 &&
1981
+ input[0] === 0x47 &&
1982
+ input[188] === 0x47 &&
1983
+ input[376] === 0x47) ||
1984
+ (input.length >= 389 &&
1985
+ input[4] === 0x47 &&
1986
+ input[196] === 0x47 &&
1987
+ input[388] === 0x47)) {
1988
+ return this.result("video", "video/mp2t", 92);
1989
+ }
1990
+ // MIDI: "MThd"
1991
+ if (input.length >= 4 && input.toString("latin1", 0, 4) === "MThd") {
1992
+ return this.result("audio", "audio/midi", 95);
1993
+ }
1994
+ // Monkey's Audio: "MAC " — the trailing space is part of the signature.
1995
+ if (input.length >= 4 && input.toString("latin1", 0, 4) === "MAC ") {
1996
+ return this.result("audio", "audio/x-ape", 95);
1997
+ }
1998
+ // WavPack: "wvpk"
1999
+ if (input.length >= 4 && input.toString("latin1", 0, 4) === "wvpk") {
2000
+ return this.result("audio", "audio/x-wavpack", 95);
2001
+ }
2002
+ // AMR narrowband ("#!AMR\n") and wideband ("#!AMR-WB\n").
2003
+ if (input.length >= 6 && input.toString("latin1", 0, 5) === "#!AMR") {
2004
+ return this.result("audio", "audio/amr", 95);
2005
+ }
2006
+ // JPEG 2000: 12-byte signature box.
2007
+ if (input.length >= 8 &&
2008
+ input[0] === 0x00 &&
2009
+ input[1] === 0x00 &&
2010
+ input[2] === 0x00 &&
2011
+ input[3] === 0x0c &&
2012
+ input.toString("latin1", 4, 8) === "jP ") {
2013
+ return this.result("image", "image/jp2", 95);
2014
+ }
1751
2015
  // MP3: ID3 tag
1752
2016
  if (input.length >= 3 &&
1753
2017
  input[0] === 0x49 &&
@@ -1779,25 +2043,69 @@ class MagicBytesStrategy {
1779
2043
  input[1] === 0x67 &&
1780
2044
  input[2] === 0x67 &&
1781
2045
  input[3] === 0x53) {
2046
+ // Ogg is a container, not a codec — .ogv (Theora/VP8 video) and .oga/.opus
2047
+ // (Vorbis/Opus/FLAC audio) all start "OggS". The codec identifier lives in
2048
+ // the first page's payload, so read it rather than assuming audio and
2049
+ // routing every Ogg video to the audio processor.
2050
+ const firstPage = input.toString("latin1", 0, Math.min(input.length, 128));
2051
+ if (firstPage.includes("theora") || firstPage.includes("VP80")) {
2052
+ return this.result("video", "video/ogg", 92);
2053
+ }
2054
+ if (firstPage.includes("OpusHead")) {
2055
+ return this.result("audio", "audio/opus", 92);
2056
+ }
1782
2057
  return this.result("audio", "audio/ogg", 90);
1783
2058
  }
1784
2059
  // ZIP: "PK\x03\x04"
1785
- // NOTE: Many document formats (OOXML: .xlsx, .docx, .pptx; ODF: .odt, .ods)
1786
- // are internally ZIP archives and share these magic bytes. We return a lower
1787
- // confidence (70%) so the ExtensionStrategy (85%) can override with the correct
1788
- // document type when a file path with extension is available. For raw buffers
1789
- // without path info, this falls through to archive as a safe default.
2060
+ // Many document formats (OOXML: .xlsx, .docx, .pptx; ODF: .odt, .ods) are
2061
+ // internally ZIP archives and share these magic bytes. Read the entry names
2062
+ // first that identifies the real format even for a bare Buffer with no
2063
+ // filename, which previously routed every Office document to the archive
2064
+ // processor. An inconclusive ZIP still reports archive at a lower
2065
+ // confidence (70%) so the ExtensionStrategy (85%) can override it.
1790
2066
  if (input.length >= 4 &&
1791
2067
  input[0] === 0x50 &&
1792
2068
  input[1] === 0x4b &&
1793
2069
  input[2] === 0x03 &&
1794
2070
  input[3] === 0x04) {
2071
+ const packaged = detectZipPackageType(input);
2072
+ if (packaged) {
2073
+ return this.result(packaged.type, packaged.mimeType, 92);
2074
+ }
1795
2075
  return this.result("archive", "application/zip", 70);
1796
2076
  }
1797
2077
  // GZIP: 1F 8B
1798
2078
  if (input.length >= 2 && input[0] === 0x1f && input[1] === 0x8b) {
1799
2079
  return this.result("archive", "application/gzip", 90);
1800
2080
  }
2081
+ // BZIP2: "BZh" + a compression-level digit.
2082
+ if (input.length >= 4 &&
2083
+ input.toString("latin1", 0, 3) === "BZh" &&
2084
+ input[3] >= 0x31 &&
2085
+ input[3] <= 0x39) {
2086
+ return this.result("archive", "application/x-bzip2", 95);
2087
+ }
2088
+ // XZ: FD "7zXZ" 00
2089
+ if (input.length >= 6 &&
2090
+ input[0] === 0xfd &&
2091
+ input.toString("latin1", 1, 5) === "7zXZ" &&
2092
+ input[5] === 0x00) {
2093
+ return this.result("archive", "application/x-xz", 95);
2094
+ }
2095
+ // Zstandard frame magic: 28 B5 2F FD
2096
+ if (input.length >= 4 &&
2097
+ input[0] === 0x28 &&
2098
+ input[1] === 0xb5 &&
2099
+ input[2] === 0x2f &&
2100
+ input[3] === 0xfd) {
2101
+ return this.result("archive", "application/zstd", 95);
2102
+ }
2103
+ // TAR: "ustar" at offset 257, inside the first header block. Checked late
2104
+ // because it is a weak, deep signature — anything with its own leading
2105
+ // magic number should have matched already.
2106
+ if (input.length >= 262 && input.toString("latin1", 257, 262) === "ustar") {
2107
+ return this.result("archive", "application/x-tar", 90);
2108
+ }
1801
2109
  // 7z: 37 7A BC AF 27 1C
1802
2110
  if (input.length >= 6 &&
1803
2111
  input[0] === 0x37 &&
@@ -1816,6 +2124,14 @@ class MagicBytesStrategy {
1816
2124
  input[3] === 0x21) {
1817
2125
  return this.result("archive", "application/x-rar-compressed", 95);
1818
2126
  }
2127
+ // SVG is text, so it has no byte signature — but it does have an
2128
+ // unambiguous root element. Without this an SVG supplied as a Buffer was
2129
+ // classified as generic XML and inlined as raw markup instead of going
2130
+ // through the SVG sanitizer, which is the whole reason SVG has its own
2131
+ // FileType. Checked last so no binary format can be beaten to it.
2132
+ if (looksLikeSvgMarkup(input)) {
2133
+ return this.result("svg", "image/svg+xml", 90);
2134
+ }
1819
2135
  return this.unknown();
1820
2136
  }
1821
2137
  isPNG(buf) {
@@ -2024,131 +2340,9 @@ class ExtensionStrategy {
2024
2340
  if (!ext) {
2025
2341
  return this.unknown();
2026
2342
  }
2027
- const typeMap = {
2028
- csv: "csv",
2029
- tsv: "csv",
2030
- jpg: "image",
2031
- jpeg: "image",
2032
- png: "image",
2033
- gif: "image",
2034
- webp: "image",
2035
- bmp: "image",
2036
- tiff: "image",
2037
- tif: "image",
2038
- // SVG is handled as text/markup, NOT as image
2039
- // AI providers don't support SVG format, so we process it as sanitized text
2040
- svg: "svg",
2041
- avif: "image",
2042
- heic: "image",
2043
- heif: "image",
2044
- pdf: "pdf",
2045
- // Video formats
2046
- mp4: "video",
2047
- mkv: "video",
2048
- mov: "video",
2049
- avi: "video",
2050
- webm: "video",
2051
- wmv: "video",
2052
- flv: "video",
2053
- // Audio formats
2054
- mp3: "audio",
2055
- wav: "audio",
2056
- ogg: "audio",
2057
- flac: "audio",
2058
- m4a: "audio",
2059
- aac: "audio",
2060
- wma: "audio",
2061
- opus: "audio",
2062
- // Archive formats
2063
- zip: "archive",
2064
- tar: "archive",
2065
- gz: "archive",
2066
- tgz: "archive",
2067
- rar: "archive",
2068
- "7z": "archive",
2069
- jar: "archive",
2070
- // Document formats (ZIP-based internally)
2071
- xlsx: "xlsx",
2072
- xls: "xlsx",
2073
- docx: "docx",
2074
- doc: "docx",
2075
- pptx: "pptx",
2076
- ppt: "pptx",
2077
- odt: "docx", // OpenDocument text → processed like docx
2078
- ods: "xlsx", // OpenDocument spreadsheet → processed like xlsx
2079
- odp: "pptx", // OpenDocument presentation → processed like pptx
2080
- rtf: "docx", // RTF → processed like docx (text extraction)
2081
- // Text/markup formats
2082
- txt: "text",
2083
- md: "text",
2084
- markdown: "text",
2085
- json: "text",
2086
- xml: "text",
2087
- yaml: "text",
2088
- yml: "text",
2089
- html: "text",
2090
- htm: "text",
2091
- css: "text",
2092
- log: "text",
2093
- conf: "text",
2094
- cfg: "text",
2095
- ini: "text",
2096
- env: "text",
2097
- toml: "text",
2098
- properties: "text",
2099
- gitignore: "text",
2100
- dockerignore: "text",
2101
- editorconfig: "text",
2102
- prettierrc: "text",
2103
- eslintrc: "text",
2104
- babelrc: "text",
2105
- // Source code formats
2106
- js: "text",
2107
- mjs: "text",
2108
- cjs: "text",
2109
- jsx: "text",
2110
- ts: "text",
2111
- tsx: "text",
2112
- py: "text",
2113
- java: "text",
2114
- go: "text",
2115
- rs: "text",
2116
- rb: "text",
2117
- php: "text",
2118
- c: "text",
2119
- cpp: "text",
2120
- cc: "text",
2121
- h: "text",
2122
- hpp: "text",
2123
- cs: "text",
2124
- swift: "text",
2125
- kt: "text",
2126
- kts: "text",
2127
- scala: "text",
2128
- sh: "text",
2129
- bash: "text",
2130
- zsh: "text",
2131
- ps1: "text",
2132
- sql: "text",
2133
- r: "text",
2134
- lua: "text",
2135
- pl: "text",
2136
- perl: "text",
2137
- dart: "text",
2138
- ex: "text",
2139
- exs: "text",
2140
- erl: "text",
2141
- hs: "text",
2142
- clj: "text",
2143
- lisp: "text",
2144
- vim: "text",
2145
- // Additional video/image
2146
- m4v: "video",
2147
- ico: "image",
2148
- };
2149
- const type = typeMap[ext.toLowerCase()];
2343
+ const type = resolveFileTypeForExtension(ext);
2150
2344
  return {
2151
- type: type || "unknown",
2345
+ type: type ?? "unknown",
2152
2346
  mimeType: this.getMimeType(ext),
2153
2347
  extension: ext,
2154
2348
  source: this.detectSource(input),
@@ -2201,127 +2395,7 @@ class ExtensionStrategy {
2201
2395
  return "path";
2202
2396
  }
2203
2397
  getMimeType(ext) {
2204
- const mimeMap = {
2205
- csv: "text/csv",
2206
- tsv: "text/tab-separated-values",
2207
- jpg: "image/jpeg",
2208
- jpeg: "image/jpeg",
2209
- png: "image/png",
2210
- gif: "image/gif",
2211
- webp: "image/webp",
2212
- bmp: "image/bmp",
2213
- tiff: "image/tiff",
2214
- tif: "image/tiff",
2215
- svg: "image/svg+xml",
2216
- avif: "image/avif",
2217
- heic: "image/heic",
2218
- heif: "image/heif",
2219
- pdf: "application/pdf",
2220
- // Video MIME types
2221
- mp4: "video/mp4",
2222
- mkv: "video/x-matroska",
2223
- mov: "video/quicktime",
2224
- avi: "video/x-msvideo",
2225
- webm: "video/webm",
2226
- wmv: "video/x-ms-wmv",
2227
- flv: "video/x-flv",
2228
- // Audio MIME types
2229
- mp3: "audio/mpeg",
2230
- wav: "audio/wav",
2231
- ogg: "audio/ogg",
2232
- flac: "audio/flac",
2233
- m4a: "audio/mp4",
2234
- aac: "audio/aac",
2235
- wma: "audio/x-ms-wma",
2236
- opus: "audio/opus",
2237
- // Archive MIME types
2238
- zip: "application/zip",
2239
- tar: "application/x-tar",
2240
- gz: "application/gzip",
2241
- tgz: "application/gzip",
2242
- rar: "application/x-rar-compressed",
2243
- "7z": "application/x-7z-compressed",
2244
- jar: "application/java-archive",
2245
- // Document MIME types
2246
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2247
- xls: "application/vnd.ms-excel",
2248
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2249
- doc: "application/msword",
2250
- pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
2251
- ppt: "application/vnd.ms-powerpoint",
2252
- odt: "application/vnd.oasis.opendocument.text",
2253
- ods: "application/vnd.oasis.opendocument.spreadsheet",
2254
- odp: "application/vnd.oasis.opendocument.presentation",
2255
- rtf: "application/rtf",
2256
- // Text/markup MIME types
2257
- txt: "text/plain",
2258
- md: "text/markdown",
2259
- markdown: "text/markdown",
2260
- json: "application/json",
2261
- xml: "application/xml",
2262
- yaml: "application/yaml",
2263
- yml: "application/yaml",
2264
- html: "text/html",
2265
- htm: "text/html",
2266
- css: "text/css",
2267
- log: "text/plain",
2268
- conf: "text/plain",
2269
- cfg: "text/plain",
2270
- ini: "text/plain",
2271
- env: "text/plain",
2272
- toml: "text/plain",
2273
- properties: "text/plain",
2274
- gitignore: "text/plain",
2275
- dockerignore: "text/plain",
2276
- editorconfig: "text/plain",
2277
- prettierrc: "application/json",
2278
- eslintrc: "application/json",
2279
- babelrc: "application/json",
2280
- // Source code MIME types
2281
- js: "text/javascript",
2282
- mjs: "text/javascript",
2283
- cjs: "text/javascript",
2284
- jsx: "text/javascript",
2285
- ts: "text/typescript",
2286
- tsx: "text/typescript",
2287
- py: "text/x-python",
2288
- java: "text/x-java-source",
2289
- go: "text/x-go",
2290
- rs: "text/x-rustsrc",
2291
- rb: "text/x-ruby",
2292
- php: "text/x-php",
2293
- c: "text/x-c",
2294
- cpp: "text/x-c++",
2295
- cc: "text/x-c++",
2296
- h: "text/x-c",
2297
- hpp: "text/x-c++",
2298
- cs: "text/x-csharp",
2299
- swift: "text/x-swift",
2300
- kt: "text/x-kotlin",
2301
- kts: "text/x-kotlin",
2302
- scala: "text/x-scala",
2303
- sh: "text/x-shellscript",
2304
- bash: "text/x-shellscript",
2305
- zsh: "text/x-shellscript",
2306
- ps1: "text/x-powershell",
2307
- sql: "text/x-sql",
2308
- r: "text/x-r",
2309
- lua: "text/x-lua",
2310
- pl: "text/x-perl",
2311
- perl: "text/x-perl",
2312
- dart: "text/x-dart",
2313
- ex: "text/x-elixir",
2314
- exs: "text/x-elixir",
2315
- erl: "text/x-erlang",
2316
- hs: "text/x-haskell",
2317
- clj: "text/x-clojure",
2318
- lisp: "text/x-lisp",
2319
- vim: "text/plain",
2320
- // Additional video/image
2321
- m4v: "video/mp4",
2322
- ico: "image/x-icon",
2323
- };
2324
- return mimeMap[ext.toLowerCase()] || "application/octet-stream";
2398
+ return getMimeTypeForExtension(ext);
2325
2399
  }
2326
2400
  unknown() {
2327
2401
  return {
@@ -2370,6 +2444,15 @@ class ContentHeuristicStrategy {
2370
2444
  else {
2371
2445
  return this.unknown();
2372
2446
  }
2447
+ // Every check below runs on a UTF-8 *decoding* of the bytes, which succeeds
2448
+ // for any input — decoding a video yields a string full of replacement
2449
+ // characters, and that string can still satisfy a text heuristic. It did:
2450
+ // MPEG-1 video decoded to "lines" with a consistent delimiter count and was
2451
+ // classified `csv`, then handed to the CSV parser. Reject binary up front so
2452
+ // no text heuristic can ever see it.
2453
+ if (ContentHeuristicStrategy.looksBinary(buffer)) {
2454
+ return this.unknown();
2455
+ }
2373
2456
  const sample = buffer.toString("utf-8", 0, Math.min(2000, buffer.length));
2374
2457
  // Check for JSON first (more specific than CSV)
2375
2458
  if (this.looksLikeJSON(sample)) {
@@ -2394,6 +2477,39 @@ class ContentHeuristicStrategy {
2394
2477
  }
2395
2478
  return this.unknown();
2396
2479
  }
2480
+ /**
2481
+ * Whether a buffer holds binary rather than text.
2482
+ *
2483
+ * Two independent signals, both computed on the same leading sample that the
2484
+ * heuristics themselves inspect:
2485
+ *
2486
+ * - a NUL byte, which no text encoding this detector supports emits (UTF-16
2487
+ * would, but it is not among the formats handled here and would already
2488
+ * have been caught by its BOM);
2489
+ * - more than 10% control/undecodable bytes, which catches binaries that
2490
+ * happen not to contain a NUL in their first 2 KB.
2491
+ *
2492
+ * Tab, newline and carriage return are text and excluded from the control
2493
+ * count.
2494
+ */
2495
+ static looksBinary(buffer) {
2496
+ const sampleLength = Math.min(2000, buffer.length);
2497
+ if (sampleLength === 0) {
2498
+ return false;
2499
+ }
2500
+ let controlBytes = 0;
2501
+ for (let i = 0; i < sampleLength; i++) {
2502
+ const byte = buffer[i];
2503
+ if (byte === 0x00) {
2504
+ return true;
2505
+ }
2506
+ const isTextWhitespace = byte === 0x09 || byte === 0x0a || byte === 0x0d;
2507
+ if (!isTextWhitespace && (byte < 0x20 || byte === 0x7f)) {
2508
+ controlBytes++;
2509
+ }
2510
+ }
2511
+ return controlBytes / sampleLength > 0.1;
2512
+ }
2397
2513
  looksLikeCSV(text) {
2398
2514
  const lines = text.trim().split("\n");
2399
2515
  if (lines.length < 2) {