@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 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
@@ -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,CAwCjE"}
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 brand = String.fromCharCode(bytes[8] ?? 0, bytes[9] ?? 0, bytes[10] ?? 0, bytes[11] ?? 0);
29
- if (brand === "avif" || brand === "avis")
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 === "mif1")
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)) {
@@ -1 +1 @@
1
- {"version":3,"file":"detect.js","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"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"}
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";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC3D,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"}
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,EAAE,cAAc,EAAiB,MAAM,aAAa,CAAA;AAC3D,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"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../src/sign.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA8DH,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"}
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;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC,CAAA;AAED,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAElD,SAAS,gBAAgB,CAAC,CAAM;IAC9B,MAAM,KAAK,GAAqC,EAAE,CAAA;IAClD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,YAAY,EAAE,CAAC;QAC1C,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAA;IAC1D,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,cAAc,CAAC,CAAM,EAAE,IAAY;IAC1C,MAAM,MAAM,GAAG,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC1C,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AACzD,CAAC;AAED,sGAAsG;AACtG,6FAA6F;AAC7F,MAAM,SAAS,GAAG,CAAC,CAAM,EAAE,GAAW,EAAU,EAAE,CAChD,GAAG,CAAC,CAAC,QAAQ,KAAK,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAA;AASjE,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAW,EACX,MAAc,EACd,OAA+B;IAE/B,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAA;IAClC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC7B,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;IAC7B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IACxD,MAAM,GAAG,GAAG,GAAG,GAAG,OAAO,CAAC,gBAAgB,CAAA;IAC1C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAClC,MAAM,EACN,MAAM,SAAS,CAAC,MAAM,CAAC,EACvB,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAC9B,CAAA;IACD,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;IACvC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,CAAA;IAC5C,OAAO,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAA;AAC9B,CAAC;AAED,iGAAiG;AACjG,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAW,EACX,MAAc,EACd,OAAmC;IAEnC,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAA;IAClC,MAAM,GAAG,GAAG,cAAc,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;IACrC,MAAM,GAAG,GAAG,cAAc,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;IACrC,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IACpE,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IACzD,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG;QAAE,OAAO,KAAK,CAAA;IACnC,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,CAAA;IACnC,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IACnC,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CACzB,MAAM,EACN,MAAM,SAAS,CAAC,MAAM,CAAC,EACvB,QAAQ,EACR,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CACtC,CAAA;AACH,CAAC"}
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.5.0",
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 brand = String.fromCharCode(bytes[8] ?? 0, bytes[9] ?? 0, bytes[10] ?? 0, bytes[11] ?? 0)
36
- if (brand === "avif" || brand === "avis") return { mime: "image/avif", ext: "avif" }
37
- if (brand.startsWith("hei") || brand === "mif1") return { mime: "image/heic", ext: "heic" }
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 { detectFileType, type FileType } from "./detect.ts"
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
  }