New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

file-type

Package Overview
Dependencies
Maintainers
1
Versions
152
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

file-type - npm Package Compare versions

Comparing version 19.6.0 to 20.0.0

371

core.d.ts

@@ -14,313 +14,2 @@ /**

export type FileExtension =
| 'jpg'
| 'png'
| 'apng'
| 'gif'
| 'webp'
| 'flif'
| 'xcf'
| 'cr2'
| 'cr3'
| 'orf'
| 'arw'
| 'dng'
| 'nef'
| 'rw2'
| 'raf'
| 'tif'
| 'bmp'
| 'icns'
| 'jxr'
| 'psd'
| 'indd'
| 'zip'
| 'tar'
| 'rar'
| 'gz'
| 'bz2'
| '7z'
| 'dmg'
| 'mp4'
| 'mid'
| 'mkv'
| 'webm'
| 'mov'
| 'avi'
| 'mpg'
| 'mp2'
| 'mp3'
| 'm4a'
| 'ogg'
| 'opus'
| 'flac'
| 'wav'
| 'qcp'
| 'amr'
| 'pdf'
| 'epub'
| 'mobi'
| 'elf'
| 'macho'
| 'exe'
| 'swf'
| 'rtf'
| 'woff'
| 'woff2'
| 'eot'
| 'ttf'
| 'otf'
| 'ico'
| 'flv'
| 'ps'
| 'xz'
| 'sqlite'
| 'nes'
| 'crx'
| 'xpi'
| 'cab'
| 'deb'
| 'ar'
| 'rpm'
| 'Z'
| 'lz'
| 'cfb'
| 'mxf'
| 'mts'
| 'wasm'
| 'blend'
| 'bpg'
| 'docx'
| 'pptx'
| 'xlsx'
| '3gp'
| '3g2'
| 'j2c'
| 'jp2'
| 'jpm'
| 'jpx'
| 'mj2'
| 'aif'
| 'odt'
| 'ods'
| 'odp'
| 'xml'
| 'heic'
| 'cur'
| 'ktx'
| 'ape'
| 'wv'
| 'asf'
| 'dcm'
| 'mpc'
| 'ics'
| 'glb'
| 'pcap'
| 'dsf'
| 'lnk'
| 'alias'
| 'voc'
| 'ac3'
| 'm4b'
| 'm4p'
| 'm4v'
| 'f4a'
| 'f4b'
| 'f4p'
| 'f4v'
| 'mie'
| 'ogv'
| 'ogm'
| 'oga'
| 'spx'
| 'ogx'
| 'arrow'
| 'shp'
| 'aac'
| 'mp1'
| 'it'
| 's3m'
| 'xm'
| 'ai'
| 'skp'
| 'avif'
| 'eps'
| 'lzh'
| 'pgp'
| 'asar'
| 'stl'
| 'chm'
| '3mf'
| 'zst'
| 'jxl'
| 'vcf'
| 'jls'
| 'pst'
| 'dwg'
| 'parquet'
| 'class'
| 'arj'
| 'cpio'
| 'ace'
| 'avro'
| 'icc'
| 'fbx'
| 'vsdx'
| 'vtt'
| 'apk'
; // eslint-disable-line semi-style
export type MimeType =
| 'image/jpeg'
| 'image/png'
| 'image/gif'
| 'image/webp'
| 'image/flif'
| 'image/x-xcf'
| 'image/x-canon-cr2'
| 'image/x-canon-cr3'
| 'image/tiff'
| 'image/bmp'
| 'image/icns'
| 'image/vnd.ms-photo'
| 'image/vnd.adobe.photoshop'
| 'application/x-indesign'
| 'application/epub+zip'
| 'application/x-xpinstall'
| 'application/vnd.oasis.opendocument.text'
| 'application/vnd.oasis.opendocument.spreadsheet'
| 'application/vnd.oasis.opendocument.presentation'
| 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
| 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
| 'application/zip'
| 'application/x-tar'
| 'application/x-rar-compressed'
| 'application/gzip'
| 'application/x-bzip2'
| 'application/x-7z-compressed'
| 'application/x-apple-diskimage'
| 'video/mp4'
| 'audio/midi'
| 'video/x-matroska'
| 'video/webm'
| 'video/quicktime'
| 'video/vnd.avi'
| 'audio/wav'
| 'audio/qcelp'
| 'audio/x-ms-asf'
| 'video/x-ms-asf'
| 'application/vnd.ms-asf'
| 'video/mpeg'
| 'video/3gpp'
| 'audio/mpeg'
| 'audio/mp4' // RFC 4337
| 'video/ogg'
| 'audio/ogg'
| 'audio/ogg; codecs=opus'
| 'application/ogg'
| 'audio/x-flac'
| 'audio/ape'
| 'audio/wavpack'
| 'audio/amr'
| 'application/pdf'
| 'application/x-elf'
| 'application/x-mach-binary'
| 'application/x-msdownload'
| 'application/x-shockwave-flash'
| 'application/rtf'
| 'application/wasm'
| 'font/woff'
| 'font/woff2'
| 'application/vnd.ms-fontobject'
| 'font/ttf'
| 'font/otf'
| 'image/x-icon'
| 'video/x-flv'
| 'application/postscript'
| 'application/eps'
| 'application/x-xz'
| 'application/x-sqlite3'
| 'application/x-nintendo-nes-rom'
| 'application/x-google-chrome-extension'
| 'application/vnd.ms-cab-compressed'
| 'application/x-deb'
| 'application/x-unix-archive'
| 'application/x-rpm'
| 'application/x-compress'
| 'application/x-lzip'
| 'application/x-cfb'
| 'application/x-mie'
| 'application/x-apache-arrow'
| 'application/mxf'
| 'video/mp2t'
| 'application/x-blender'
| 'image/bpg'
| 'image/j2c'
| 'image/jp2'
| 'image/jpx'
| 'image/jpm'
| 'image/mj2'
| 'audio/aiff'
| 'application/xml'
| 'application/x-mobipocket-ebook'
| 'image/heif'
| 'image/heif-sequence'
| 'image/heic'
| 'image/heic-sequence'
| 'image/ktx'
| 'application/dicom'
| 'audio/x-musepack'
| 'text/calendar'
| 'text/vcard'
| 'text/vtt'
| 'model/gltf-binary'
| 'application/vnd.tcpdump.pcap'
| 'audio/x-dsf' // Non-standard
| 'application/x.ms.shortcut' // Invented by us
| 'application/x.apple.alias' // Invented by us
| 'audio/x-voc'
| 'audio/vnd.dolby.dd-raw'
| 'audio/x-m4a'
| 'image/apng'
| 'image/x-olympus-orf'
| 'image/x-sony-arw'
| 'image/x-adobe-dng'
| 'image/x-nikon-nef'
| 'image/x-panasonic-rw2'
| 'image/x-fujifilm-raf'
| 'video/x-m4v'
| 'video/3gpp2'
| 'application/x-esri-shape'
| 'audio/aac'
| 'audio/x-it'
| 'audio/x-s3m'
| 'audio/x-xm'
| 'video/MP1S'
| 'video/MP2P'
| 'application/vnd.sketchup.skp'
| 'image/avif'
| 'application/x-lzh-compressed'
| 'application/pgp-encrypted'
| 'application/x-asar'
| 'model/stl'
| 'application/vnd.ms-htmlhelp'
| 'model/3mf'
| 'image/jxl'
| 'application/zstd'
| 'image/jls'
| 'application/vnd.ms-outlook'
| 'image/vnd.dwg'
| 'application/x-parquet'
| 'application/java-vm'
| 'application/x-arj'
| 'application/x-cpio'
| 'application/x-ace-compressed'
| 'application/avro'
| 'application/vnd.iccprofile'
| 'application/x.autodesk.fbx'
| 'application/vnd.visio'
| 'application/vnd.android.package-archive'
; // eslint-disable-line semi-style
export type FileTypeResult = {

@@ -330,3 +19,3 @@ /**

*/
readonly ext: FileExtension;
readonly ext: string;

@@ -336,3 +25,3 @@ /**

*/
readonly mime: MimeType;
readonly mime: string;
};

@@ -393,3 +82,3 @@

*/
export const supportedExtensions: ReadonlySet<FileExtension>;
export const supportedExtensions: ReadonlySet<string>;

@@ -399,3 +88,3 @@ /**

*/
export const supportedMimeTypes: ReadonlySet<MimeType>;
export const supportedMimeTypes: ReadonlySet<string>;

@@ -433,19 +122,23 @@ export type StreamOptions = {

/**
Function that allows specifying custom detection mechanisms.
A custom file type detector.
An iterable of detectors can be provided via the `fileTypeOptions` argument for the {@link FileTypeParser.constructor}.
Detectors can be added via the constructor options or by directly modifying `FileTypeParser#detectors`.
The detectors are called before the default detections in the provided order.
Detectors provided through the constructor options are executed before the default detectors.
Custom detectors can be used to add new `FileTypeResults` or to modify return behavior of existing `FileTypeResult` detections.
Custom detectors allow for:
- Introducing new `FileTypeResult` entries.
- Modifying the detection behavior of existing `FileTypeResult` types.
If the detector returns `undefined`, there are 2 possible scenarios:
### Detector execution flow
1. The detector has not read from the tokenizer, it will be proceeded with the next available detector.
2. The detector has read from the tokenizer (`tokenizer.position` has been increased).
In that case no further detectors will be executed and the final conclusion is that file-type returns undefined.
Note that this an exceptional scenario, as the detector takes the opportunity from any other detector to determine the file type.
If a detector returns `undefined`, the following rules apply:
Example detector array which can be extended and provided via the fileTypeOptions argument:
1. **No Tokenizer Interaction**: If the detector does not modify the tokenizer's position, the next detector in the sequence is executed.
2. **Tokenizer Interaction**: If the detector modifies the tokenizer's position (`tokenizer.position` is advanced), no further detectors are executed. In this case, the file type remains `undefined`, as subsequent detectors cannot evaluate the content. This is an exceptional scenario, as it prevents any other detectors from determining the file type.
### Example usage
Below is an example of a custom detector array. This can be passed to the `FileTypeParser` via the `fileTypeOptions` argument.
```

@@ -456,5 +149,5 @@ import {FileTypeParser} from 'file-type';

async tokenizer => {
const unicornHeader = [85, 78, 73, 67, 79, 82, 78]; // 'UNICORN' as decimal string
const unicornHeader = [85, 78, 73, 67, 79, 82, 78]; // "UNICORN" in ASCII decimal
const buffer = Buffer.alloc(7);
const buffer = new Uint8Array(unicornHeader.length);
await tokenizer.peekBuffer(buffer, {length: unicornHeader.length, mayBeLess: true});

@@ -469,13 +162,16 @@ if (unicornHeader.every((value, index) => value === buffer[index])) {

const buffer = Buffer.from('UNICORN');
const buffer = new Uint8Array([85, 78, 73, 67, 79, 82, 78]);
const parser = new FileTypeParser({customDetectors});
const fileType = await parser.fromBuffer(buffer);
console.log(fileType);
console.log(fileType); // {ext: 'unicorn', mime: 'application/unicorn'}
```
@param tokenizer - The [tokenizer](https://github.com/Borewit/strtok3#tokenizer) used to read the file content from.
@param fileType - The file type detected by the standard detections or a previous custom detection, or `undefined`` if no matching file type could be found.
@returns The detected file type, or `undefined` when there is no match.
@param tokenizer - The [tokenizer](https://github.com/Borewit/strtok3#tokenizer) used to read file content.
@param fileType - The file type detected by standard or previous custom detectors, or `undefined` if no match is found.
@returns The detected file type, or `undefined` if no match is found.
*/
export type Detector = (tokenizer: ITokenizer, fileType?: FileTypeResult) => Promise<FileTypeResult | undefined>;
export type Detector = {
id: string;
detect: (tokenizer: ITokenizer, fileType?: FileTypeResult) => Promise<FileTypeResult | undefined>;
};

@@ -502,6 +198,11 @@ export type FileTypeOptions = {

export declare class FileTypeParser {
detectors: Iterable<Detector>;
/**
File type detectors.
constructor(options?: {customDetectors?: Iterable<Detector>; signal: AbortSignal});
Initialized with a single entry holding the built-in detector function.
*/
detectors: Detector[];
constructor(options?: {customDetectors?: Iterable<Detector>; signal?: AbortSignal});
/**

@@ -508,0 +209,0 @@ Works the same way as {@link fileTypeFromBuffer}, additionally taking into account custom detectors (if any were provided to the constructor).

544

core.js

@@ -7,3 +7,4 @@ /**

import * as strtok3 from 'strtok3/core';
import {includes, indexOf, getUintBE} from 'uint8array-extras';
import {ZipHandler} from '@tokenizer/inflate';
import {includes, getUintBE} from 'uint8array-extras';
import {

@@ -30,2 +31,123 @@ stringToBytes,

function getFileTypeFromMimeType(mimeType) {
switch (mimeType) {
case 'application/epub+zip':
return {
ext: 'epub',
mime: 'application/epub+zip',
};
case 'application/vnd.oasis.opendocument.text':
return {
ext: 'odt',
mime: 'application/vnd.oasis.opendocument.text',
};
case 'application/vnd.oasis.opendocument.text-template':
return {
ext: 'ott',
mime: 'application/vnd.oasis.opendocument.text-template',
};
case 'application/vnd.oasis.opendocument.spreadsheet':
return {
ext: 'ods',
mime: 'application/vnd.oasis.opendocument.spreadsheet',
};
case 'application/vnd.oasis.opendocument.spreadsheet-template':
return {
ext: 'ots',
mime: 'application/vnd.oasis.opendocument.spreadsheet-template',
};
case 'application/vnd.oasis.opendocument.presentation':
return {
ext: 'odp',
mime: 'application/vnd.oasis.opendocument.presentation',
};
case 'application/vnd.oasis.opendocument.presentation-template':
return {
ext: 'otp',
mime: 'application/vnd.oasis.opendocument.presentation-template',
};
case 'application/vnd.oasis.opendocument.graphics':
return {
ext: 'odg',
mime: 'application/vnd.oasis.opendocument.graphics',
};
case 'application/vnd.oasis.opendocument.graphics-template':
return {
ext: 'otg',
mime: 'application/vnd.oasis.opendocument.graphics-template',
};
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
return {
ext: 'xlsx',
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
};
case 'application/vnd.ms-excel.sheet.macroEnabled':
return {
ext: 'xlsm',
mime: 'application/vnd.ms-excel.sheet.macroEnabled.12',
};
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.template':
return {
ext: 'xltx',
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
};
case 'application/vnd.ms-excel.template.macroEnabled':
return {
ext: 'xltm',
mime: 'application/vnd.ms-excel.template.macroenabled.12',
};
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
return {
ext: 'docx',
mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
};
case 'application/vnd.ms-word.document.macroEnabled':
return {
ext: 'docm',
mime: 'application/vnd.ms-word.document.macroEnabled.12',
};
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.template':
return {
ext: 'dotx',
mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
};
case 'application/vnd.ms-word.template.macroEnabledTemplate':
return {
ext: 'dotm',
mime: 'application/vnd.ms-word.template.macroEnabled.12',
};
case 'application/vnd.openxmlformats-officedocument.presentationml.template':
return {
ext: 'potx',
mime: 'application/vnd.openxmlformats-officedocument.presentationml.template',
};
case 'application/vnd.ms-powerpoint.template.macroEnabled':
return {
ext: 'potm',
mime: 'application/vnd.ms-powerpoint.template.macroEnabled.12',
};
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
return {
ext: 'pptx',
mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
};
case 'application/vnd.ms-powerpoint.presentation.macroEnabled':
return {
ext: 'pptm',
mime: 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
};
case 'application/vnd.ms-visio.drawing':
return {
ext: 'vsdx',
mime: 'application/vnd.visio',
};
case 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml':
return {
ext: '3mf',
mime: 'model/3mf',
};
default:
}
}
function _check(buffer, headers, options) {

@@ -62,9 +184,8 @@ options = {

constructor(options) {
this.detectors = options?.customDetectors;
this.detectors = [...(options?.customDetectors ?? []),
{id: 'core', detect: this.detectConfident},
{id: 'core.imprecise', detect: this.detectImprecise}];
this.tokenizerOptions = {
abortSignal: options?.signal,
};
this.fromTokenizer = this.fromTokenizer.bind(this);
this.fromBuffer = this.fromBuffer.bind(this);
this.parse = this.parse.bind(this);
}

@@ -75,4 +196,5 @@

for (const detector of this.detectors || []) {
const fileType = await detector(tokenizer);
// Iterate through all file-type detectors
for (const detector of this.detectors) {
const fileType = await detector.detect(tokenizer);
if (fileType) {

@@ -86,4 +208,2 @@ return fileType;

}
return this.parse(tokenizer);
}

@@ -171,3 +291,4 @@

async parse(tokenizer) {
// Detections with a high degree of certainty in identifying the correct file type
detectConfident = async tokenizer => {
this.buffer = new Uint8Array(reasonableDetectionSizeInBytes);

@@ -262,3 +383,3 @@

this.tokenizer.ignore(3);
return this.parse(tokenizer);
return this.detectConfident(tokenizer);
}

@@ -397,136 +518,65 @@

if (this.check([0x50, 0x4B, 0x3, 0x4])) { // Local file header signature
try {
while (tokenizer.position + 30 < tokenizer.fileInfo.size) {
await tokenizer.readBuffer(this.buffer, {length: 30});
const view = new DataView(this.buffer.buffer);
// https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers
const zipHeader = {
compressedSize: view.getUint32(18, true),
uncompressedSize: view.getUint32(22, true),
filenameLength: view.getUint16(26, true),
extraFieldLength: view.getUint16(28, true),
};
zipHeader.filename = await tokenizer.readToken(new Token.StringType(zipHeader.filenameLength, 'utf-8'));
await tokenizer.ignore(zipHeader.extraFieldLength);
if (/classes\d*\.dex/.test(zipHeader.filename)) {
let fileType;
await new ZipHandler(tokenizer).unzip(zipHeader => {
switch (zipHeader.filename) {
case 'META-INF/mozilla.rsa':
fileType = {
ext: 'xpi',
mime: 'application/x-xpinstall',
};
return {
ext: 'apk',
mime: 'application/vnd.android.package-archive',
stop: true,
};
}
// Assumes signed `.xpi` from addons.mozilla.org
if (zipHeader.filename === 'META-INF/mozilla.rsa') {
case 'META-INF/MANIFEST.MF':
fileType = {
ext: 'jar',
mime: 'application/java-archive',
};
return {
ext: 'xpi',
mime: 'application/x-xpinstall',
stop: true,
};
}
if (zipHeader.filename.endsWith('.rels') || zipHeader.filename.endsWith('.xml')) {
const type = zipHeader.filename.split('/')[0];
switch (type) {
case '_rels':
break;
case 'word':
return {
ext: 'docx',
mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
};
case 'ppt':
return {
ext: 'pptx',
mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
};
case 'xl':
return {
ext: 'xlsx',
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
};
case 'visio':
return {
ext: 'vsdx',
mime: 'application/vnd.visio',
};
default:
break;
}
}
if (zipHeader.filename.startsWith('xl/')) {
case 'mimetype':
return {
ext: 'xlsx',
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
async handler(fileData) {
// Use TextDecoder to decode the UTF-8 encoded data
const mimeType = new TextDecoder('utf-8').decode(fileData).trim();
fileType = getFileTypeFromMimeType(mimeType);
},
stop: true,
};
}
if (zipHeader.filename.startsWith('3D/') && zipHeader.filename.endsWith('.model')) {
case '[Content_Types].xml':
return {
ext: '3mf',
mime: 'model/3mf',
async handler(fileData) {
// Use TextDecoder to decode the UTF-8 encoded data
let xmlContent = new TextDecoder('utf-8').decode(fileData);
const endPos = xmlContent.indexOf('.main+xml"');
if (endPos === -1) {
const mimeType = 'application/vnd.ms-package.3dmanufacturing-3dmodel+xml';
if (xmlContent.includes(`ContentType="${mimeType}"`)) {
fileType = getFileTypeFromMimeType(mimeType);
}
} else {
xmlContent = xmlContent.slice(0, Math.max(0, endPos));
const firstPos = xmlContent.lastIndexOf('"');
const mimeType = xmlContent.slice(Math.max(0, firstPos + 1));
fileType = getFileTypeFromMimeType(mimeType);
}
},
stop: true,
};
}
// The docx, xlsx and pptx file types extend the Office Open XML file format:
// https://en.wikipedia.org/wiki/Office_Open_XML_file_formats
// We look for:
// - one entry named '[Content_Types].xml' or '_rels/.rels',
// - one entry indicating specific type of file.
// MS Office, OpenOffice and LibreOffice may put the parts in different order, so the check should not rely on it.
if (zipHeader.filename === 'mimetype' && zipHeader.compressedSize === zipHeader.uncompressedSize) {
let mimeType = await tokenizer.readToken(new Token.StringType(zipHeader.compressedSize, 'utf-8'));
mimeType = mimeType.trim();
switch (mimeType) {
case 'application/epub+zip':
return {
ext: 'epub',
mime: 'application/epub+zip',
};
case 'application/vnd.oasis.opendocument.text':
return {
ext: 'odt',
mime: 'application/vnd.oasis.opendocument.text',
};
case 'application/vnd.oasis.opendocument.spreadsheet':
return {
ext: 'ods',
mime: 'application/vnd.oasis.opendocument.spreadsheet',
};
case 'application/vnd.oasis.opendocument.presentation':
return {
ext: 'odp',
mime: 'application/vnd.oasis.opendocument.presentation',
};
default:
default:
if (/classes\d*\.dex/.test(zipHeader.filename)) {
fileType = {
ext: 'apk',
mime: 'application/vnd.android.package-archive',
};
return {stop: true};
}
}
// Try to find next header manually when current one is corrupted
if (zipHeader.compressedSize === 0) {
let nextHeaderIndex = -1;
while (nextHeaderIndex < 0 && (tokenizer.position < tokenizer.fileInfo.size)) {
await tokenizer.peekBuffer(this.buffer, {mayBeLess: true});
nextHeaderIndex = indexOf(this.buffer, new Uint8Array([0x50, 0x4B, 0x03, 0x04]));
// Move position to the next header if found, skip the whole buffer otherwise
await tokenizer.ignore(nextHeaderIndex >= 0 ? nextHeaderIndex : this.buffer.length);
}
} else {
await tokenizer.ignore(zipHeader.compressedSize);
}
return {};
}
} catch (error) {
if (!(error instanceof strtok3.EndOfStreamError)) {
throw error;
}
}
});
return {
return fileType ?? {
ext: 'zip',

@@ -609,64 +659,2 @@ mime: 'application/zip',

//
// File Type Box (https://en.wikipedia.org/wiki/ISO_base_media_file_format)
// It's not required to be first, but it's recommended to be. Almost all ISO base media files start with `ftyp` box.
// `ftyp` box must contain a brand major identifier, which must consist of ISO 8859-1 printable characters.
// Here we check for 8859-1 printable characters (for simplicity, it's a mask which also catches one non-printable character).
if (
this.checkString('ftyp', {offset: 4})
&& (this.buffer[8] & 0x60) !== 0x00 // Brand major, first character ASCII?
) {
// They all can have MIME `video/mp4` except `application/mp4` special-case which is hard to detect.
// For some cases, we're specific, everything else falls to `video/mp4` with `mp4` extension.
const brandMajor = new Token.StringType(4, 'latin1').get(this.buffer, 8).replace('\0', ' ').trim();
switch (brandMajor) {
case 'avif':
case 'avis':
return {ext: 'avif', mime: 'image/avif'};
case 'mif1':
return {ext: 'heic', mime: 'image/heif'};
case 'msf1':
return {ext: 'heic', mime: 'image/heif-sequence'};
case 'heic':
case 'heix':
return {ext: 'heic', mime: 'image/heic'};
case 'hevc':
case 'hevx':
return {ext: 'heic', mime: 'image/heic-sequence'};
case 'qt':
return {ext: 'mov', mime: 'video/quicktime'};
case 'M4V':
case 'M4VH':
case 'M4VP':
return {ext: 'm4v', mime: 'video/x-m4v'};
case 'M4P':
return {ext: 'm4p', mime: 'video/mp4'};
case 'M4B':
return {ext: 'm4b', mime: 'audio/mp4'};
case 'M4A':
return {ext: 'm4a', mime: 'audio/x-m4a'};
case 'F4V':
return {ext: 'f4v', mime: 'video/mp4'};
case 'F4P':
return {ext: 'f4p', mime: 'video/mp4'};
case 'F4A':
return {ext: 'f4a', mime: 'audio/mp4'};
case 'F4B':
return {ext: 'f4b', mime: 'audio/mp4'};
case 'crx':
return {ext: 'cr3', mime: 'image/x-canon-cr3'};
default:
if (brandMajor.startsWith('3g')) {
if (brandMajor.startsWith('3g2')) {
return {ext: '3g2', mime: 'video/3gpp2'};
}
return {ext: '3gp', mime: 'video/3gpp'};
}
return {ext: 'mp4', mime: 'video/mp4'};
}
}
if (this.checkString('MThd')) {

@@ -853,5 +841,5 @@ return {

const re = await readElement();
const docType = await readChildren(re.len);
const documentType = await readChildren(re.len);
switch (docType) {
switch (documentType) {
case 'webm':

@@ -979,2 +967,9 @@ return {

if (this.check([0x04, 0x22, 0x4D, 0x18])) {
return {
ext: 'lz4',
mime: 'application/x-lz4', // Invented by us
};
}
// -- 5-byte signatures --

@@ -1070,2 +1065,9 @@

if (this.checkString('DRACO')) {
return {
ext: 'drc',
mime: 'application/vnd.google.draco', // Invented by us
};
}
// -- 6-byte signatures --

@@ -1256,2 +1258,62 @@

// File Type Box (https://en.wikipedia.org/wiki/ISO_base_media_file_format)
// It's not required to be first, but it's recommended to be. Almost all ISO base media files start with `ftyp` box.
// `ftyp` box must contain a brand major identifier, which must consist of ISO 8859-1 printable characters.
// Here we check for 8859-1 printable characters (for simplicity, it's a mask which also catches one non-printable character).
if (
this.checkString('ftyp', {offset: 4})
&& (this.buffer[8] & 0x60) !== 0x00 // Brand major, first character ASCII?
) {
// They all can have MIME `video/mp4` except `application/mp4` special-case which is hard to detect.
// For some cases, we're specific, everything else falls to `video/mp4` with `mp4` extension.
const brandMajor = new Token.StringType(4, 'latin1').get(this.buffer, 8).replace('\0', ' ').trim();
switch (brandMajor) {
case 'avif':
case 'avis':
return {ext: 'avif', mime: 'image/avif'};
case 'mif1':
return {ext: 'heic', mime: 'image/heif'};
case 'msf1':
return {ext: 'heic', mime: 'image/heif-sequence'};
case 'heic':
case 'heix':
return {ext: 'heic', mime: 'image/heic'};
case 'hevc':
case 'hevx':
return {ext: 'heic', mime: 'image/heic-sequence'};
case 'qt':
return {ext: 'mov', mime: 'video/quicktime'};
case 'M4V':
case 'M4VH':
case 'M4VP':
return {ext: 'm4v', mime: 'video/x-m4v'};
case 'M4P':
return {ext: 'm4p', mime: 'video/mp4'};
case 'M4B':
return {ext: 'm4b', mime: 'audio/mp4'};
case 'M4A':
return {ext: 'm4a', mime: 'audio/x-m4a'};
case 'F4V':
return {ext: 'f4v', mime: 'video/mp4'};
case 'F4P':
return {ext: 'f4p', mime: 'video/mp4'};
case 'F4A':
return {ext: 'f4a', mime: 'audio/mp4'};
case 'F4B':
return {ext: 'f4b', mime: 'audio/mp4'};
case 'crx':
return {ext: 'cr3', mime: 'image/x-canon-cr3'};
default:
if (brandMajor.startsWith('3g')) {
if (brandMajor.startsWith('3g2')) {
return {ext: '3g2', mime: 'video/3gpp2'};
}
return {ext: '3gp', mime: 'video/3gpp'};
}
return {ext: 'mp4', mime: 'video/mp4'};
}
}
// -- 12-byte signatures --

@@ -1396,35 +1458,2 @@

// -- Unsafe signatures --
if (
this.check([0x0, 0x0, 0x1, 0xBA])
|| this.check([0x0, 0x0, 0x1, 0xB3])
) {
return {
ext: 'mpg',
mime: 'video/mpeg',
};
}
if (this.check([0x00, 0x01, 0x00, 0x00, 0x00])) {
return {
ext: 'ttf',
mime: 'font/ttf',
};
}
if (this.check([0x00, 0x00, 0x01, 0x00])) {
return {
ext: 'ico',
mime: 'image/x-icon',
};
}
if (this.check([0x00, 0x00, 0x02, 0x00])) {
return {
ext: 'cur',
mime: 'image/x-icon',
};
}
if (this.check([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1])) {

@@ -1635,3 +1664,41 @@ // Detected Microsoft Compound File Binary File (MS-CFB) Format.

}
};
// Detections with limited supporting data, resulting in a higher likelihood of false positives
detectImprecise = async tokenizer => {
this.buffer = new Uint8Array(reasonableDetectionSizeInBytes);
// Read initial sample size of 8 bytes
await tokenizer.peekBuffer(this.buffer, {length: Math.min(8, tokenizer.fileInfo.size), mayBeLess: true});
if (
this.check([0x0, 0x0, 0x1, 0xBA])
|| this.check([0x0, 0x0, 0x1, 0xB3])
) {
return {
ext: 'mpg',
mime: 'video/mpeg',
};
}
if (this.check([0x00, 0x01, 0x00, 0x00, 0x00])) {
return {
ext: 'ttf',
mime: 'font/ttf',
};
}
if (this.check([0x00, 0x00, 0x01, 0x00])) {
return {
ext: 'ico',
mime: 'image/x-icon',
};
}
if (this.check([0x00, 0x00, 0x02, 0x00])) {
return {
ext: 'cur',
mime: 'image/x-icon',
};
}
// Check MPEG 1 or 2 Layer 3 header, or 'layer 0' for ADTS (MPEG sync-word 0xFFE)

@@ -1680,3 +1747,3 @@ if (this.buffer.length >= 2 && this.check([0xFF, 0xE0], {offset: 0, mask: [0xFF, 0xE0]})) {

}
}
};

@@ -1725,7 +1792,14 @@ async readTiffTag(bigEndian) {

if (ifdOffset >= 8 && (this.check([0x1C, 0x00, 0xFE, 0x00], {offset: 8}) || this.check([0x1F, 0x00, 0x0B, 0x00], {offset: 8}))) {
return {
ext: 'nef',
mime: 'image/x-nikon-nef',
};
if (ifdOffset >= 8) {
const someId1 = (bigEndian ? Token.UINT16_BE : Token.UINT16_LE).get(this.buffer, 8);
const someId2 = (bigEndian ? Token.UINT16_BE : Token.UINT16_LE).get(this.buffer, 10);
if (
(someId1 === 0x1C && someId2 === 0xFE)
|| (someId1 === 0x1F && someId2 === 0x0B)) {
return {
ext: 'nef',
mime: 'image/x-nikon-nef',
};
}
}

@@ -1732,0 +1806,0 @@ }

@@ -7,4 +7,10 @@ /**

import type {AnyWebByteStream} from 'strtok3';
import type {FileTypeResult, StreamOptions, AnyWebReadableStream, Detector, AnyWebReadableByteStreamWithFileType} from './core.js';
import {FileTypeParser} from './core.js';
import {
type FileTypeResult,
type StreamOptions,
type AnyWebReadableStream,
type Detector,
type AnyWebReadableByteStreamWithFileType,
FileTypeParser as DefaultFileTypeParser,
} from './core.js';

@@ -18,3 +24,3 @@ export type ReadableStreamWithFileType = NodeReadableStream & {

*/
export declare class NodeFileTypeParser extends FileTypeParser {
export declare class FileTypeParser extends DefaultFileTypeParser {
/**

@@ -21,0 +27,0 @@ @param stream - Node.js `stream.Readable` or web `ReadableStream`.

@@ -8,5 +8,5 @@ /**

import * as strtok3 from 'strtok3';
import {FileTypeParser, reasonableDetectionSizeInBytes} from './core.js';
import {FileTypeParser as DefaultFileTypeParser, reasonableDetectionSizeInBytes} from './core.js';
export class NodeFileTypeParser extends FileTypeParser {
export class FileTypeParser extends DefaultFileTypeParser {
async fromStream(stream) {

@@ -70,13 +70,19 @@ const tokenizer = await (stream instanceof WebReadableStream ? strtok3.fromWebStream(stream, this.tokenizerOptions) : strtok3.fromStream(stream, this.tokenizerOptions));

export async function fileTypeFromFile(path, fileTypeOptions) {
return (new NodeFileTypeParser(fileTypeOptions)).fromFile(path, fileTypeOptions);
return (new FileTypeParser(fileTypeOptions)).fromFile(path, fileTypeOptions);
}
export async function fileTypeFromStream(stream, fileTypeOptions) {
return (new NodeFileTypeParser(fileTypeOptions)).fromStream(stream);
return (new FileTypeParser(fileTypeOptions)).fromStream(stream);
}
export async function fileTypeStream(readableStream, options = {}) {
return new NodeFileTypeParser(options).toDetectionStream(readableStream, options);
return new FileTypeParser(options).toDetectionStream(readableStream, options);
}
export {fileTypeFromTokenizer, fileTypeFromBuffer, fileTypeFromBlob, FileTypeParser, supportedMimeTypes, supportedExtensions} from './core.js';
export {
fileTypeFromTokenizer,
fileTypeFromBuffer,
fileTypeFromBlob,
supportedMimeTypes,
supportedExtensions,
} from './core.js';
{
"name": "file-type",
"version": "19.6.0",
"version": "20.0.0",
"description": "Detect the file type of a file, stream, or data",

@@ -220,18 +220,36 @@ "license": "MIT",

"vtt",
"apk"
"apk",
"drc",
"lz4",
"potx",
"xltx",
"dotx",
"xltm",
"ots",
"odg",
"otg",
"otp",
"ott",
"xlsm",
"docm",
"dotm",
"potm",
"pptm",
"jar"
],
"dependencies": {
"get-stream": "^9.0.1",
"strtok3": "^9.0.1",
"@tokenizer/inflate": "^0.2.6",
"strtok3": "^10.0.1",
"token-types": "^6.0.0",
"uint8array-extras": "^1.3.0"
"uint8array-extras": "^1.4.0"
},
"devDependencies": {
"@tokenizer/token": "^0.3.0",
"@types/node": "^20.10.7",
"@types/node": "^22.10.5",
"ava": "^6.0.1",
"commonmark": "^0.30.0",
"commonmark": "^0.31.2",
"get-stream": "^9.0.1",
"noop-stream": "^1.0.0",
"tsd": "^0.30.3",
"xo": "^0.56.0"
"tsd": "^0.31.2",
"xo": "^0.60.0"
},

@@ -238,0 +256,0 @@ "xo": {

@@ -19,3 +19,3 @@ <h1 align="center" title="file-type">

**This package is an ESM package. Your project needs to be ESM too. [Read more](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).**
**This package is an ESM package. Your project needs to be ESM too. [Read more](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c). For TypeScript + CommonJS, see [`load-esm`](https://github.com/Borewit/load-esm).**

@@ -26,3 +26,3 @@ If you use it with Webpack, you need the latest Webpack version and ensure you configure it correctly for ESM.

#### Node.js
### Node.js

@@ -96,3 +96,3 @@ Determine file type from a file:

#### Browser
### Browser

@@ -184,6 +184,6 @@ ```js

[!TIP]
> [!TIP]
> A [`File` object](https://developer.mozilla.org/docs/Web/API/File) is a `Blob` and can be passed in here.
It will **stream** the underlying Blob, and required a [ReadableStreamBYOBReader](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader) which **require Node.js ≥ 20**.
It will **stream** the underlying Blob.

@@ -211,2 +211,17 @@ The file type is detected by checking the [magic number](https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files) of the blob.

> [!WARNING]
> This method depends on [ReadableStreamBYOBReader](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader) which **requires Node.js ≥ 20**
> and [may not be available in all modern browsers](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamBYOBReader#browser_compatibility).
To work around this limitation, you can use an alternative approach to read and process the `Blob` without relying on streaming:
```js
import {fileTypeFromBuffer} from 'file-type';
async function readFromBlobWithoutStreaming(blob) {
const buffer = await blob.arrayBuffer();
return fileTypeFromBuffer(buffer);
}
```
#### blob

@@ -333,30 +348,33 @@

A custom detector is a function that allows specifying custom detection mechanisms.
A custom file type detector.
An iterable of detectors can be provided via the `fileTypeOptions` argument for the `FileTypeParser` constructor.
In Node.js, you should use `NodeFileTypeParser`, which extends `FileTypeParser` and provides access to Node.js specific functions.
Detectors can be added via the constructor options or by directly modifying `FileTypeParser#detectors`.
The detectors are called before the default detections in the provided order.
Detectors provided through the constructor options are executed before the default detectors.
Custom detectors can be used to add new `FileTypeResults` or to modify return behaviour of existing `FileTypeResult` detections.
Custom detectors allow for:
- Introducing new `FileTypeResult` entries.
- Modifying the detection behavior of existing `FileTypeResult` types.
If the detector returns `undefined`, there are 2 possible scenarios:
### Detector execution flow
1. The detector has not read from the tokenizer, it will be proceeded with the next available detector.
2. The detector has read from the tokenizer (`tokenizer.position` has been increased).
In that case no further detectors will be executed and the final conclusion is that file-type returns undefined.
Note that this an exceptional scenario, as the detector takes the opportunity from any other detector to determine the file type.
If a detector returns `undefined`, the following rules apply:
Example detector array which can be extended and provided to each public method via the `fileTypeOptions` argument:
1. **No Tokenizer Interaction**: If the detector does not modify the tokenizer's position, the next detector in the sequence is executed.
2. **Tokenizer Interaction**: If the detector modifies the tokenizer's position (`tokenizer.position` is advanced), no further detectors are executed. In this case, the file type remains `undefined`, as subsequent detectors cannot evaluate the content. This is an exceptional scenario, as it prevents any other detectors from determining the file type.
### Example usage
Below is an example of a custom detector array. This can be passed to the `FileTypeParser` via the `fileTypeOptions` argument.
```js
import {FileTypeParser} from 'file-type'; // or `NodeFileTypeParser` in Node.js
import {FileTypeParser} from 'file-type';
const customDetectors = [
async tokenizer => {
const unicornHeader = [85, 78, 73, 67, 79, 82, 78]; // 'UNICORN' as decimal string
const unicornDetector = {
id: 'unicorn', // May be used to recognize the detector in the detector list
async detect(tokenizer) {
const unicornHeader = [85, 78, 73, 67, 79, 82, 78]; // "UNICORN" in ASCII decimal
const buffer = new Uint8Array(7);
const buffer = new Uint8Array(unicornHeader.length);
await tokenizer.peekBuffer(buffer, {length: unicornHeader.length, mayBeLess: true});
if (unicornHeader.every((value, index) => value === buffer[index])) {

@@ -367,11 +385,20 @@ return {ext: 'unicorn', mime: 'application/unicorn'};

return undefined;
},
];
}
}
const buffer = new Uint8Array(new TextEncoder().encode('UNICORN'));
const parser = new FileTypeParser({customDetectors}); // `NodeFileTypeParser({customDetectors})` in Node.js
const buffer = new Uint8Array([85, 78, 73, 67, 79, 82, 78]);
const parser = new FileTypeParser({customDetectors: [unicornDetector]});
const fileType = await parser.fromBuffer(buffer);
console.log(fileType);
console.log(fileType); // {ext: 'unicorn', mime: 'application/unicorn'}
```
```ts
/**
@param tokenizer - The [tokenizer](https://github.com/Borewit/strtok3#tokenizer) used to read file content.
@param fileType - The file type detected by standard or previous custom detectors, or `undefined` if no match is found.
@returns The detected file type, or `undefined` if no match is found.
*/
export type Detector = (tokenizer: ITokenizer, fileType?: FileTypeResult) => Promise<FileTypeResult | undefined>;
```
## Abort signal

@@ -382,3 +409,3 @@

```js
import {FileTypeParser} from 'file-type'; // or `NodeFileTypeParser` in Node.js
import {FileTypeParser} from 'file-type';

@@ -437,3 +464,7 @@ const abortController = new AbortController()

- [`dng`](https://en.wikipedia.org/wiki/Digital_Negative) - Adobe Digital Negative image file
- [`docm`](https://en.wikipedia.org/wiki/List_of_Microsoft_Office_filename_extensions) - Microsoft Word macro-enabled document
- [`docx`](https://en.wikipedia.org/wiki/Office_Open_XML) - Microsoft Word document
- [`dotm`](https://en.wikipedia.org/wiki/List_of_Microsoft_Office_filename_extensions) - Microsoft Word macro-enabled template
- [`dotx`](https://en.wikipedia.org/wiki/List_of_Microsoft_Office_filename_extensions) - Microsoft Word template
- [`drc`](https://en.wikipedia.org/wiki/Zstandard) - Google's Draco 3D Data Compression
- [`dsf`](https://dsd-guide.com/sites/default/files/white-papers/DSFFileFormatSpec_E.pdf) - Sony DSD Stream File (DSF)

@@ -465,2 +496,3 @@ - [`dwg`](https://en.wikipedia.org/wiki/.dwg) - Autodesk CAD file

- [`j2c`](https://en.wikipedia.org/wiki/JPEG_2000) - JPEG 2000
- [`jar`](https://en.wikipedia.org/wiki/JAR_(file_format)) - Java archive
- [`jls`](https://en.wikipedia.org/wiki/Lossless_JPEG#JPEG-LS) - Lossless/near-lossless compression standard for continuous-tone images

@@ -476,2 +508,3 @@ - [`jp2`](https://en.wikipedia.org/wiki/JPEG_2000) - JPEG 2000

- [`lz`](https://en.wikipedia.org/wiki/Lzip) - Archive file
- [`lz4`](https://en.wikipedia.org/wiki/LZ4_(compression_algorithm)) - Compressed archive created by one of a variety of LZ4 compression utilities
- [`lzh`](https://en.wikipedia.org/wiki/LHA_(file_format)) - LZH archive

@@ -499,2 +532,3 @@ - [`m4a`](https://en.wikipedia.org/wiki/M4A) - Audio-only MPEG-4 files

- [`nes`](https://fileinfo.com/extension/nes) - Nintendo NES ROM
- [`odg`](https://en.wikipedia.org/wiki/OpenDocument) - OpenDocument for drawing
- [`odp`](https://en.wikipedia.org/wiki/OpenDocument) - OpenDocument for presentations

@@ -511,2 +545,6 @@ - [`ods`](https://en.wikipedia.org/wiki/OpenDocument) - OpenDocument for spreadsheets

- [`otf`](https://en.wikipedia.org/wiki/OpenType) - OpenType font
- [`otg`](https://en.wikipedia.org/wiki/OpenDocument_technical_specification#Templates) - OpenDocument template for drawing
- [`otp`](https://en.wikipedia.org/wiki/OpenDocument_technical_specification#Templates) - OpenDocument template for presentations
- [`ots`](https://en.wikipedia.org/wiki/OpenDocument_technical_specification#Templates) - OpenDocument template for spreadsheets
- [`ott`](https://en.wikipedia.org/wiki/OpenDocument_technical_specification#Templates) - OpenDocument template for word processing
- [`parquet`](https://en.wikipedia.org/wiki/Apache_Parquet) - Apache Parquet

@@ -517,3 +555,6 @@ - [`pcap`](https://wiki.wireshark.org/Development/LibpcapFileFormat) - Libpcap File Format

- [`png`](https://en.wikipedia.org/wiki/Portable_Network_Graphics) - Portable Network Graphics
- [`pptx`](https://en.wikipedia.org/wiki/Office_Open_XML) - Microsoft Powerpoint document
- [`potm`](https://en.wikipedia.org/wiki/List_of_Microsoft_Office_filename_extensions) - Microsoft PowerPoint macro-enabled template
- [`potx`](https://en.wikipedia.org/wiki/List_of_Microsoft_Office_filename_extensions) - Microsoft PowerPoint template
- [`pptm`](https://en.wikipedia.org/wiki/List_of_Microsoft_Office_filename_extensions) - Microsoft PowerPoint macro-enabled document
- [`pptx`](https://en.wikipedia.org/wiki/Office_Open_XML) - Microsoft PowerPoint document
- [`ps`](https://en.wikipedia.org/wiki/Postscript) - Postscript

@@ -550,3 +591,6 @@ - [`psd`](https://en.wikipedia.org/wiki/Adobe_Photoshop#File_format) - Adobe Photoshop document

- [`xcf`](https://en.wikipedia.org/wiki/XCF_(file_format)) - eXperimental Computing Facility
- [`xlsm`](https://en.wikipedia.org/wiki/List_of_Microsoft_Office_filename_extensions) - Microsoft Excel macro-enabled document
- [`xlsx`](https://en.wikipedia.org/wiki/Office_Open_XML) - Microsoft Excel document
- [`xltm`](https://en.wikipedia.org/wiki/List_of_Microsoft_Office_filename_extensions) - Microsoft Excel macro-enabled template
- [`xltx`](https://en.wikipedia.org/wiki/List_of_Microsoft_Office_filename_extensions) - Microsoft Excel template
- [`xm`](https://wiki.openmpt.org/Manual:_Module_formats#The_FastTracker_2_format_.28.xm.29) - Audio module format: FastTracker 2

@@ -553,0 +597,0 @@ - [`xml`](https://en.wikipedia.org/wiki/XML) - eXtensible Markup Language

@@ -157,2 +157,19 @@ export const extensions = [

'apk',
'drc',
'lz4',
'potx',
'xltx',
'dotx',
'xltm',
'ott',
'ots',
'otp',
'odg',
'otg',
'xlsm',
'docm',
'dotm',
'potm',
'pptm',
'jar',
];

@@ -311,2 +328,19 @@

'application/vnd.android.package-archive',
'application/vnd.google.draco', // Invented by us
'application/x-lz4', // Invented by us
'application/vnd.openxmlformats-officedocument.presentationml.template',
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'application/vnd.ms-excel.template.macroenabled.12',
'application/vnd.oasis.opendocument.text-template',
'application/vnd.oasis.opendocument.spreadsheet-template',
'application/vnd.oasis.opendocument.presentation-template',
'application/vnd.oasis.opendocument.graphics',
'application/vnd.oasis.opendocument.graphics-template',
'application/vnd.ms-excel.sheet.macroEnabled.12',
'application/vnd.ms-word.document.macroEnabled.12',
'application/vnd.ms-word.template.macroEnabled.12',
'application/vnd.ms-powerpoint.template.macroEnabled.12',
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
'application/java-archive',
];
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc