@nifrajs/uploads 3.5.0 → 4.0.2
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.
- package/README.md +9 -0
- package/dist/detect.d.ts +12 -0
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +60 -3
- package/dist/detect.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/sign.d.ts.map +1 -1
- package/dist/sign.js +6 -1
- package/dist/sign.js.map +1 -1
- package/package.json +9 -1
- package/src/detect.ts +71 -3
- package/src/index.ts +6 -1
- package/src/sign.ts +5 -1
package/README.md
CHANGED
|
@@ -10,6 +10,15 @@ Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework - one
|
|
|
10
10
|
bun add @nifrajs/uploads
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## With a schema
|
|
14
|
+
|
|
15
|
+
`validateUpload` is for a route that reads the upload itself. To declare the file in the route's
|
|
16
|
+
body schema instead - validated before the handler runs, typed in `c.body` - use `t.file` and
|
|
17
|
+
`t.form` from `@nifrajs/schema/form`, which run the same signature detection.
|
|
18
|
+
|
|
19
|
+
`@nifrajs/uploads/detect` exports the detection alone (`detectFileType`, `DETECTABLE_MIME_TYPES`,
|
|
20
|
+
`FILE_TYPE_PREFIX_BYTES`) for code that needs the type and nothing else.
|
|
21
|
+
|
|
13
22
|
## Docs
|
|
14
23
|
|
|
15
24
|
- Reference: <https://nifra.dev/docs>
|
package/dist/detect.d.ts
CHANGED
|
@@ -10,6 +10,18 @@ export interface FileType {
|
|
|
10
10
|
/** Canonical extension (no dot), e.g. `"png"`. */
|
|
11
11
|
readonly ext: string;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* How many leading bytes {@link detectFileType} looks at. Reading this many from the start of a file
|
|
15
|
+
* (`file.slice(0, FILE_TYPE_PREFIX_BYTES)`) is enough for every type it knows: it reaches the first four
|
|
16
|
+
* compatible brands of an ISO-BMFF `ftyp` box, where an AVIF with the generic `mif1` brand says `avif`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const FILE_TYPE_PREFIX_BYTES = 32;
|
|
19
|
+
/**
|
|
20
|
+
* Every MIME type {@link detectFileType} can return. An allow-list entry outside this set (and
|
|
21
|
+
* outside a `type/*` wildcard that covers one of them) can never match, so check a configured
|
|
22
|
+
* allow-list against it up front instead of rejecting every upload at request time.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DETECTABLE_MIME_TYPES: readonly string[];
|
|
13
25
|
/** Detect a file's type from its magic bytes, or `null` if unrecognized. */
|
|
14
26
|
export declare function detectFileType(bytes: Uint8Array): FileType | null;
|
|
15
27
|
//# sourceMappingURL=detect.d.ts.map
|
package/dist/detect.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,QAAQ;IACvB,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kDAAkD;IAClD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,QAAQ,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,QAAQ;IACvB,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kDAAkD;IAClD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAA;AAExC;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAiBlD,CAAA;AAiBD,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,QAAQ,GAAG,IAAI,CA8DjE"}
|
package/dist/detect.js
CHANGED
|
@@ -4,6 +4,50 @@
|
|
|
4
4
|
* types; returns `null` for anything unrecognized (incl. text formats like SVG/CSV that have no magic
|
|
5
5
|
* number - handle those explicitly if you accept them).
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* How many leading bytes {@link detectFileType} looks at. Reading this many from the start of a file
|
|
9
|
+
* (`file.slice(0, FILE_TYPE_PREFIX_BYTES)`) is enough for every type it knows: it reaches the first four
|
|
10
|
+
* compatible brands of an ISO-BMFF `ftyp` box, where an AVIF with the generic `mif1` brand says `avif`.
|
|
11
|
+
*/
|
|
12
|
+
export const FILE_TYPE_PREFIX_BYTES = 32;
|
|
13
|
+
/**
|
|
14
|
+
* Every MIME type {@link detectFileType} can return. An allow-list entry outside this set (and
|
|
15
|
+
* outside a `type/*` wildcard that covers one of them) can never match, so check a configured
|
|
16
|
+
* allow-list against it up front instead of rejecting every upload at request time.
|
|
17
|
+
*/
|
|
18
|
+
export const DETECTABLE_MIME_TYPES = [
|
|
19
|
+
"image/jpeg",
|
|
20
|
+
"image/png",
|
|
21
|
+
"image/gif",
|
|
22
|
+
"image/webp",
|
|
23
|
+
"audio/wav",
|
|
24
|
+
"video/x-msvideo",
|
|
25
|
+
"image/avif",
|
|
26
|
+
"image/heic",
|
|
27
|
+
"audio/mp4",
|
|
28
|
+
"video/mp4",
|
|
29
|
+
"video/webm",
|
|
30
|
+
"audio/ogg",
|
|
31
|
+
"audio/mpeg",
|
|
32
|
+
"application/pdf",
|
|
33
|
+
"application/zip",
|
|
34
|
+
"application/gzip",
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* An MPEG audio frame header: an 11-bit frame sync, then a version, layer, bitrate and sample rate that
|
|
38
|
+
* are not reserved values. An MP3 needs no ID3 tag, so this is how an untagged one starts. AAC's ADTS
|
|
39
|
+
* sync shares the first 12 bits, but its layer is always the reserved `00`.
|
|
40
|
+
*/
|
|
41
|
+
function mpegAudioFrame(bytes) {
|
|
42
|
+
const [b0 = 0, b1 = 0, b2 = 0] = bytes;
|
|
43
|
+
if (bytes.length < 4 || b0 !== 0xff || (b1 & 0xe0) !== 0xe0)
|
|
44
|
+
return false;
|
|
45
|
+
const version = (b1 >> 3) & 0b11;
|
|
46
|
+
const layer = (b1 >> 1) & 0b11;
|
|
47
|
+
const bitrate = b2 >> 4;
|
|
48
|
+
const sampleRate = (b2 >> 2) & 0b11;
|
|
49
|
+
return version !== 0b01 && layer !== 0b00 && bitrate !== 0b1111 && sampleRate !== 0b11;
|
|
50
|
+
}
|
|
7
51
|
/** Detect a file's type from its magic bytes, or `null` if unrecognized. */
|
|
8
52
|
export function detectFileType(bytes) {
|
|
9
53
|
const at = (offset, ...sig) => sig.every((byte, i) => bytes[offset + i] === byte);
|
|
@@ -25,11 +69,22 @@ export function detectFileType(bytes) {
|
|
|
25
69
|
}
|
|
26
70
|
// ISO-BMFF (`ftyp` box at offset 4) - mp4 / avif / heic / m4a, disambiguated by the major brand.
|
|
27
71
|
if (at(4, 0x66, 0x74, 0x79, 0x70)) {
|
|
28
|
-
const
|
|
29
|
-
|
|
72
|
+
const brandAt = (offset) => String.fromCharCode(bytes[offset] ?? 0, bytes[offset + 1] ?? 0, bytes[offset + 2] ?? 0, bytes[offset + 3] ?? 0);
|
|
73
|
+
const brand = brandAt(8);
|
|
74
|
+
// The compatible brands follow the minor version, to the end of the ftyp box.
|
|
75
|
+
const boxEnd = Math.min(bytes.length, ((bytes[0] ?? 0) << 24) | ((bytes[1] ?? 0) << 16) | ((bytes[2] ?? 0) << 8) | (bytes[3] ?? 0));
|
|
76
|
+
const compatible = [];
|
|
77
|
+
for (let offset = 16; offset + 4 <= boxEnd; offset += 4)
|
|
78
|
+
compatible.push(brandAt(offset));
|
|
79
|
+
// A HEIF-family file whose major brand is the generic `mif1`/`msf1`/`miaf` names its codec among
|
|
80
|
+
// the compatible brands: AVIF says `avif`/`avis`.
|
|
81
|
+
const avif = (name) => name === "avif" || name === "avis";
|
|
82
|
+
const generic = brand === "mif1" || brand === "msf1" || brand === "miaf";
|
|
83
|
+
if (avif(brand) || (generic && compatible.some(avif)))
|
|
30
84
|
return { mime: "image/avif", ext: "avif" };
|
|
31
|
-
if (brand.startsWith("hei") || brand
|
|
85
|
+
if (brand.startsWith("hei") || brand.startsWith("hev") || generic) {
|
|
32
86
|
return { mime: "image/heic", ext: "heic" };
|
|
87
|
+
}
|
|
33
88
|
// Audio brands share the ISO-BMFF container but are NOT video - labeling them `video/mp4` would
|
|
34
89
|
// both reject real audio under an `audio/*` allow-list and admit it under `video/*`. `M4A `/`M4B `
|
|
35
90
|
// are AAC audio / audiobooks.
|
|
@@ -44,6 +99,8 @@ export function detectFileType(bytes) {
|
|
|
44
99
|
return { mime: "audio/ogg", ext: "ogg" };
|
|
45
100
|
if (at(0, 0x49, 0x44, 0x33))
|
|
46
101
|
return { mime: "audio/mpeg", ext: "mp3" }; // ID3 tag
|
|
102
|
+
if (mpegAudioFrame(bytes))
|
|
103
|
+
return { mime: "audio/mpeg", ext: "mp3" }; // an untagged MP3
|
|
47
104
|
if (at(0, 0x25, 0x50, 0x44, 0x46))
|
|
48
105
|
return { mime: "application/pdf", ext: "pdf" }; // "%PDF"
|
|
49
106
|
if (at(0, 0x50, 0x4b, 0x03, 0x04) || at(0, 0x50, 0x4b, 0x05, 0x06)) {
|
package/dist/detect.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect.js","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,KAAiB;IAC9C,MAAM,EAAE,GAAG,CAAC,MAAc,EAAE,GAAG,GAAa,EAAW,EAAE,CACvD,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAA;IAEpD,SAAS;IACT,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;IACtE,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QACvD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;IAC1C,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;IAE3E,wDAAwD;IACxD,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QAClC,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,CAAA;QAC7E,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;QAC3E,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;IACnF,CAAC;IAED,iGAAiG;IACjG,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QAClC,MAAM,
|
|
1
|
+
{"version":3,"file":"detect.js","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - {@link signDownloadUrl} / {@link verifyDownloadUrl} - short-TTL, tamper-evident download URLs.
|
|
8
8
|
* - {@link stripImageMetadata} - drop EXIF/GPS by re-encoding via any `@nifrajs/image` backend.
|
|
9
9
|
*/
|
|
10
|
-
export { detectFileType, type FileType } from "./detect.js";
|
|
10
|
+
export { DETECTABLE_MIME_TYPES, detectFileType, FILE_TYPE_PREFIX_BYTES, type FileType, } from "./detect.js";
|
|
11
11
|
export { type SignDownloadUrlOptions, signDownloadUrl, verifyDownloadUrl, } from "./sign.js";
|
|
12
12
|
export { type ImageReencoder, type StripImageMetadataOptions, stripImageMetadata, } from "./strip.js";
|
|
13
13
|
export { type UploadResult, type ValidateUploadOptions, validateUpload, } from "./validate.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,qBAAqB,EACrB,cAAc,EACd,sBAAsB,EACtB,KAAK,QAAQ,GACd,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,sBAAsB,EAC3B,eAAe,EACf,iBAAiB,GAClB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,yBAAyB,EAC9B,kBAAkB,GACnB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC1B,cAAc,GACf,MAAM,eAAe,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - {@link signDownloadUrl} / {@link verifyDownloadUrl} - short-TTL, tamper-evident download URLs.
|
|
8
8
|
* - {@link stripImageMetadata} - drop EXIF/GPS by re-encoding via any `@nifrajs/image` backend.
|
|
9
9
|
*/
|
|
10
|
-
export { detectFileType } from "./detect.js";
|
|
10
|
+
export { DETECTABLE_MIME_TYPES, detectFileType, FILE_TYPE_PREFIX_BYTES, } from "./detect.js";
|
|
11
11
|
export { signDownloadUrl, verifyDownloadUrl, } from "./sign.js";
|
|
12
12
|
export { stripImageMetadata, } from "./strip.js";
|
|
13
13
|
export { validateUpload, } from "./validate.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,qBAAqB,EACrB,cAAc,EACd,sBAAsB,GAEvB,MAAM,aAAa,CAAA;AACpB,OAAO,EAEL,eAAe,EACf,iBAAiB,GAClB,MAAM,WAAW,CAAA;AAClB,OAAO,EAGL,kBAAkB,GACnB,MAAM,YAAY,CAAA;AACnB,OAAO,EAGL,cAAc,GACf,MAAM,eAAe,CAAA"}
|
package/dist/sign.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAkEH,MAAM,WAAW,sBAAsB;IACrC,qCAAqC;IACrC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,wFAAwF;IACxF,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,8EAA8E;AAC9E,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED,iGAAiG;AACjG,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAClC,OAAO,CAAC,OAAO,CAAC,CAelB"}
|
package/dist/sign.js
CHANGED
|
@@ -23,13 +23,18 @@ const toBase64Url = (buf) => {
|
|
|
23
23
|
s += String.fromCharCode(byte);
|
|
24
24
|
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
25
25
|
};
|
|
26
|
+
// Canonical unpadded base64url only. `atob` skips whitespace and ignores a final group's unused bits,
|
|
27
|
+
// so without the round-trip check one signature has several spellings - several distinct URLs (cache
|
|
28
|
+
// keys, log lines, dedupe keys) that all verify.
|
|
26
29
|
const fromBase64Url = (value) => {
|
|
30
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value))
|
|
31
|
+
return null;
|
|
27
32
|
try {
|
|
28
33
|
const b = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
|
|
29
34
|
const out = new Uint8Array(b.length);
|
|
30
35
|
for (let i = 0; i < b.length; i++)
|
|
31
36
|
out[i] = b.charCodeAt(i);
|
|
32
|
-
return out;
|
|
37
|
+
return toBase64Url(out.buffer) === value ? out : null;
|
|
33
38
|
}
|
|
34
39
|
catch {
|
|
35
40
|
return null;
|
package/dist/sign.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sign.js","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAA;AAC7B,MAAM,UAAU,GAAG,kBAAkB,CAAA,CAAC,+DAA+D;AAErG,0FAA0F;AAC1F,2FAA2F;AAC3F,MAAM,gBAAgB,GAAG,EAAE,CAAA;AAE3B,MAAM,SAAS,GAAG,CAAC,MAAc,EAAsB,EAAE;IACvD,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,gBAAgB,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CACb,mDAAmD,gBAAgB,8DAA8D,CAClI,CAAA;IACH,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,SAAS,CAC5B,KAAK,EACL,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAClB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,EAAE,QAAQ,CAAC,CACnB,CAAA;AACH,CAAC,CAAA;AAED,MAAM,WAAW,GAAG,CAAC,GAAgB,EAAU,EAAE;IAC/C,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAA;IACjC,IAAI,CAAC,GAAG,EAAE,CAAA;IACV,KAAK,MAAM,IAAI,IAAI,KAAK;QAAE,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;IACxD,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AAC3E,CAAC,CAAA;AAED,MAAM,aAAa,GAAG,CAAC,KAAa,EAAkC,EAAE;IACtE,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAA;QAC3D,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QAC3D,OAAO,GAAG,CAAA;
|
|
1
|
+
{"version":3,"file":"sign.js","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/uploads",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "File-upload hardening for nifra - magic-byte MIME detection, size + type validation, short-TTL signed download URLs, and EXIF/metadata stripping. Dependency-free, edge-safe.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"magic-bytes"
|
|
17
17
|
],
|
|
18
18
|
"type": "module",
|
|
19
|
+
"nifra": {
|
|
20
|
+
"environment": "library"
|
|
21
|
+
},
|
|
19
22
|
"main": "./dist/index.js",
|
|
20
23
|
"module": "./dist/index.js",
|
|
21
24
|
"types": "./dist/index.d.ts",
|
|
@@ -24,6 +27,11 @@
|
|
|
24
27
|
"bun": "./src/index.ts",
|
|
25
28
|
"types": "./dist/index.d.ts",
|
|
26
29
|
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./detect": {
|
|
32
|
+
"bun": "./src/detect.ts",
|
|
33
|
+
"types": "./dist/detect.d.ts",
|
|
34
|
+
"default": "./dist/detect.js"
|
|
27
35
|
}
|
|
28
36
|
},
|
|
29
37
|
"files": [
|
package/src/detect.ts
CHANGED
|
@@ -12,6 +12,52 @@ export interface FileType {
|
|
|
12
12
|
readonly ext: string
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* How many leading bytes {@link detectFileType} looks at. Reading this many from the start of a file
|
|
17
|
+
* (`file.slice(0, FILE_TYPE_PREFIX_BYTES)`) is enough for every type it knows: it reaches the first four
|
|
18
|
+
* compatible brands of an ISO-BMFF `ftyp` box, where an AVIF with the generic `mif1` brand says `avif`.
|
|
19
|
+
*/
|
|
20
|
+
export const FILE_TYPE_PREFIX_BYTES = 32
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Every MIME type {@link detectFileType} can return. An allow-list entry outside this set (and
|
|
24
|
+
* outside a `type/*` wildcard that covers one of them) can never match, so check a configured
|
|
25
|
+
* allow-list against it up front instead of rejecting every upload at request time.
|
|
26
|
+
*/
|
|
27
|
+
export const DETECTABLE_MIME_TYPES: readonly string[] = [
|
|
28
|
+
"image/jpeg",
|
|
29
|
+
"image/png",
|
|
30
|
+
"image/gif",
|
|
31
|
+
"image/webp",
|
|
32
|
+
"audio/wav",
|
|
33
|
+
"video/x-msvideo",
|
|
34
|
+
"image/avif",
|
|
35
|
+
"image/heic",
|
|
36
|
+
"audio/mp4",
|
|
37
|
+
"video/mp4",
|
|
38
|
+
"video/webm",
|
|
39
|
+
"audio/ogg",
|
|
40
|
+
"audio/mpeg",
|
|
41
|
+
"application/pdf",
|
|
42
|
+
"application/zip",
|
|
43
|
+
"application/gzip",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* An MPEG audio frame header: an 11-bit frame sync, then a version, layer, bitrate and sample rate that
|
|
48
|
+
* are not reserved values. An MP3 needs no ID3 tag, so this is how an untagged one starts. AAC's ADTS
|
|
49
|
+
* sync shares the first 12 bits, but its layer is always the reserved `00`.
|
|
50
|
+
*/
|
|
51
|
+
function mpegAudioFrame(bytes: Uint8Array): boolean {
|
|
52
|
+
const [b0 = 0, b1 = 0, b2 = 0] = bytes
|
|
53
|
+
if (bytes.length < 4 || b0 !== 0xff || (b1 & 0xe0) !== 0xe0) return false
|
|
54
|
+
const version = (b1 >> 3) & 0b11
|
|
55
|
+
const layer = (b1 >> 1) & 0b11
|
|
56
|
+
const bitrate = b2 >> 4
|
|
57
|
+
const sampleRate = (b2 >> 2) & 0b11
|
|
58
|
+
return version !== 0b01 && layer !== 0b00 && bitrate !== 0b1111 && sampleRate !== 0b11
|
|
59
|
+
}
|
|
60
|
+
|
|
15
61
|
/** Detect a file's type from its magic bytes, or `null` if unrecognized. */
|
|
16
62
|
export function detectFileType(bytes: Uint8Array): FileType | null {
|
|
17
63
|
const at = (offset: number, ...sig: number[]): boolean =>
|
|
@@ -32,9 +78,30 @@ export function detectFileType(bytes: Uint8Array): FileType | null {
|
|
|
32
78
|
|
|
33
79
|
// ISO-BMFF (`ftyp` box at offset 4) - mp4 / avif / heic / m4a, disambiguated by the major brand.
|
|
34
80
|
if (at(4, 0x66, 0x74, 0x79, 0x70)) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
81
|
+
const brandAt = (offset: number): string =>
|
|
82
|
+
String.fromCharCode(
|
|
83
|
+
bytes[offset] ?? 0,
|
|
84
|
+
bytes[offset + 1] ?? 0,
|
|
85
|
+
bytes[offset + 2] ?? 0,
|
|
86
|
+
bytes[offset + 3] ?? 0,
|
|
87
|
+
)
|
|
88
|
+
const brand = brandAt(8)
|
|
89
|
+
// The compatible brands follow the minor version, to the end of the ftyp box.
|
|
90
|
+
const boxEnd = Math.min(
|
|
91
|
+
bytes.length,
|
|
92
|
+
((bytes[0] ?? 0) << 24) | ((bytes[1] ?? 0) << 16) | ((bytes[2] ?? 0) << 8) | (bytes[3] ?? 0),
|
|
93
|
+
)
|
|
94
|
+
const compatible: string[] = []
|
|
95
|
+
for (let offset = 16; offset + 4 <= boxEnd; offset += 4) compatible.push(brandAt(offset))
|
|
96
|
+
// A HEIF-family file whose major brand is the generic `mif1`/`msf1`/`miaf` names its codec among
|
|
97
|
+
// the compatible brands: AVIF says `avif`/`avis`.
|
|
98
|
+
const avif = (name: string): boolean => name === "avif" || name === "avis"
|
|
99
|
+
const generic = brand === "mif1" || brand === "msf1" || brand === "miaf"
|
|
100
|
+
if (avif(brand) || (generic && compatible.some(avif)))
|
|
101
|
+
return { mime: "image/avif", ext: "avif" }
|
|
102
|
+
if (brand.startsWith("hei") || brand.startsWith("hev") || generic) {
|
|
103
|
+
return { mime: "image/heic", ext: "heic" }
|
|
104
|
+
}
|
|
38
105
|
// Audio brands share the ISO-BMFF container but are NOT video - labeling them `video/mp4` would
|
|
39
106
|
// both reject real audio under an `audio/*` allow-list and admit it under `video/*`. `M4A `/`M4B `
|
|
40
107
|
// are AAC audio / audiobooks.
|
|
@@ -46,6 +113,7 @@ export function detectFileType(bytes: Uint8Array): FileType | null {
|
|
|
46
113
|
if (at(0, 0x1a, 0x45, 0xdf, 0xa3)) return { mime: "video/webm", ext: "webm" } // Matroska/WebM
|
|
47
114
|
if (at(0, 0x4f, 0x67, 0x67, 0x53)) return { mime: "audio/ogg", ext: "ogg" }
|
|
48
115
|
if (at(0, 0x49, 0x44, 0x33)) return { mime: "audio/mpeg", ext: "mp3" } // ID3 tag
|
|
116
|
+
if (mpegAudioFrame(bytes)) return { mime: "audio/mpeg", ext: "mp3" } // an untagged MP3
|
|
49
117
|
if (at(0, 0x25, 0x50, 0x44, 0x46)) return { mime: "application/pdf", ext: "pdf" } // "%PDF"
|
|
50
118
|
if (at(0, 0x50, 0x4b, 0x03, 0x04) || at(0, 0x50, 0x4b, 0x05, 0x06)) {
|
|
51
119
|
return { mime: "application/zip", ext: "zip" } // also docx/xlsx/odt containers
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
* - {@link signDownloadUrl} / {@link verifyDownloadUrl} - short-TTL, tamper-evident download URLs.
|
|
8
8
|
* - {@link stripImageMetadata} - drop EXIF/GPS by re-encoding via any `@nifrajs/image` backend.
|
|
9
9
|
*/
|
|
10
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
DETECTABLE_MIME_TYPES,
|
|
12
|
+
detectFileType,
|
|
13
|
+
FILE_TYPE_PREFIX_BYTES,
|
|
14
|
+
type FileType,
|
|
15
|
+
} from "./detect.ts"
|
|
11
16
|
export {
|
|
12
17
|
type SignDownloadUrlOptions,
|
|
13
18
|
signDownloadUrl,
|
package/src/sign.ts
CHANGED
|
@@ -35,12 +35,16 @@ const toBase64Url = (buf: ArrayBuffer): string => {
|
|
|
35
35
|
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Canonical unpadded base64url only. `atob` skips whitespace and ignores a final group's unused bits,
|
|
39
|
+
// so without the round-trip check one signature has several spellings - several distinct URLs (cache
|
|
40
|
+
// keys, log lines, dedupe keys) that all verify.
|
|
38
41
|
const fromBase64Url = (value: string): Uint8Array<ArrayBuffer> | null => {
|
|
42
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value)) return null
|
|
39
43
|
try {
|
|
40
44
|
const b = atob(value.replace(/-/g, "+").replace(/_/g, "/"))
|
|
41
45
|
const out = new Uint8Array(b.length)
|
|
42
46
|
for (let i = 0; i < b.length; i++) out[i] = b.charCodeAt(i)
|
|
43
|
-
return out
|
|
47
|
+
return toBase64Url(out.buffer) === value ? out : null
|
|
44
48
|
} catch {
|
|
45
49
|
return null
|
|
46
50
|
}
|