@nifrajs/uploads 2.2.0 → 2.3.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.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @nifrajs/uploads
2
2
 
3
- 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.
3
+ 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.
4
4
 
5
- Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework — one core, five UI libraries, every runtime. Scaffold a new app with `bun create nifra`.
5
+ Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework - one core, five UI libraries, every runtime. Scaffold a new app with `bun create nifra`.
6
6
 
7
7
  ## Install
8
8
 
@@ -19,7 +19,7 @@ MIT
19
19
 
20
20
  ## For AI agents
21
21
 
22
- Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
22
+ Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
23
23
  one cheap read instead of the whole corpus. For the wider framework: the repo's
24
24
  [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
25
25
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
package/dist/detect.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Magic-byte file-type detection — trust the bytes, not the `Content-Type` header (which a client
2
+ * Magic-byte file-type detection - trust the bytes, not the `Content-Type` header (which a client
3
3
  * sets freely). Reads only the leading bytes; dependency-free + edge-safe. Covers the common upload
4
4
  * types; returns `null` for anything unrecognized (incl. text formats like SVG/CSV that have no magic
5
- * number — handle those explicitly if you accept them).
5
+ * number - handle those explicitly if you accept them).
6
6
  */
7
7
  export interface FileType {
8
8
  /** Detected MIME type, e.g. `"image/png"`. */
package/dist/detect.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Magic-byte file-type detection — trust the bytes, not the `Content-Type` header (which a client
2
+ * Magic-byte file-type detection - trust the bytes, not the `Content-Type` header (which a client
3
3
  * sets freely). Reads only the leading bytes; dependency-free + edge-safe. Covers the common upload
4
4
  * types; returns `null` for anything unrecognized (incl. text formats like SVG/CSV that have no magic
5
- * number — handle those explicitly if you accept them).
5
+ * number - handle those explicitly if you accept them).
6
6
  */
7
7
  /** Detect a file's type from its magic bytes, or `null` if unrecognized. */
8
8
  export function detectFileType(bytes) {
@@ -14,7 +14,7 @@ export function detectFileType(bytes) {
14
14
  return { mime: "image/png", ext: "png" };
15
15
  if (at(0, 0x47, 0x49, 0x46, 0x38))
16
16
  return { mime: "image/gif", ext: "gif" };
17
- // RIFF container (bytes 0–3 "RIFF", 8–11 the form type)
17
+ // RIFF container (bytes 0-3 "RIFF", 8-11 the form type)
18
18
  if (at(0, 0x52, 0x49, 0x46, 0x46)) {
19
19
  if (at(8, 0x57, 0x45, 0x42, 0x50))
20
20
  return { mime: "image/webp", ext: "webp" };
@@ -23,14 +23,14 @@ export function detectFileType(bytes) {
23
23
  if (at(8, 0x41, 0x56, 0x49, 0x20))
24
24
  return { mime: "video/x-msvideo", ext: "avi" };
25
25
  }
26
- // ISO-BMFF (`ftyp` box at offset 4) — mp4 / avif / heic / m4a, disambiguated by the major brand.
26
+ // ISO-BMFF (`ftyp` box at offset 4) - mp4 / avif / heic / m4a, disambiguated by the major brand.
27
27
  if (at(4, 0x66, 0x74, 0x79, 0x70)) {
28
28
  const brand = String.fromCharCode(bytes[8] ?? 0, bytes[9] ?? 0, bytes[10] ?? 0, bytes[11] ?? 0);
29
29
  if (brand === "avif" || brand === "avis")
30
30
  return { mime: "image/avif", ext: "avif" };
31
31
  if (brand.startsWith("hei") || brand === "mif1")
32
32
  return { mime: "image/heic", ext: "heic" };
33
- // Audio brands share the ISO-BMFF container but are NOT video — labeling them `video/mp4` would
33
+ // Audio brands share the ISO-BMFF container but are NOT video - labeling them `video/mp4` would
34
34
  // both reject real audio under an `audio/*` allow-list and admit it under `video/*`. `M4A `/`M4B `
35
35
  // are AAC audio / audiobooks.
36
36
  if (brand === "M4A " || brand === "M4B ")
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * `@nifrajs/uploads` — file-upload hardening, dependency-free + edge-safe.
2
+ * `@nifrajs/uploads` - file-upload hardening, dependency-free + edge-safe.
3
3
  *
4
- * - {@link detectFileType} — sniff the real type from magic bytes (not the client's `Content-Type`).
5
- * - {@link validateUpload} — size cap + magic-byte type allow-list. Pair with `@nifrajs/core`'s
4
+ * - {@link detectFileType} - sniff the real type from magic bytes (not the client's `Content-Type`).
5
+ * - {@link validateUpload} - size cap + magic-byte type allow-list. Pair with `@nifrajs/core`'s
6
6
  * `c.boundedBody(maxBytes)` to also bound the read.
7
- * - {@link signDownloadUrl} / {@link verifyDownloadUrl} — short-TTL, tamper-evident download URLs.
8
- * - {@link stripImageMetadata} — drop EXIF/GPS by re-encoding via any `@nifrajs/image` backend.
7
+ * - {@link signDownloadUrl} / {@link verifyDownloadUrl} - short-TTL, tamper-evident download URLs.
8
+ * - {@link stripImageMetadata} - drop EXIF/GPS by re-encoding via any `@nifrajs/image` backend.
9
9
  */
10
10
  export { detectFileType, type FileType } from "./detect.js";
11
11
  export { type SignDownloadUrlOptions, signDownloadUrl, verifyDownloadUrl, } from "./sign.js";
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * `@nifrajs/uploads` — file-upload hardening, dependency-free + edge-safe.
2
+ * `@nifrajs/uploads` - file-upload hardening, dependency-free + edge-safe.
3
3
  *
4
- * - {@link detectFileType} — sniff the real type from magic bytes (not the client's `Content-Type`).
5
- * - {@link validateUpload} — size cap + magic-byte type allow-list. Pair with `@nifrajs/core`'s
4
+ * - {@link detectFileType} - sniff the real type from magic bytes (not the client's `Content-Type`).
5
+ * - {@link validateUpload} - size cap + magic-byte type allow-list. Pair with `@nifrajs/core`'s
6
6
  * `c.boundedBody(maxBytes)` to also bound the read.
7
- * - {@link signDownloadUrl} / {@link verifyDownloadUrl} — short-TTL, tamper-evident download URLs.
8
- * - {@link stripImageMetadata} — drop EXIF/GPS by re-encoding via any `@nifrajs/image` backend.
7
+ * - {@link signDownloadUrl} / {@link verifyDownloadUrl} - short-TTL, tamper-evident download URLs.
8
+ * - {@link stripImageMetadata} - drop EXIF/GPS by re-encoding via any `@nifrajs/image` backend.
9
9
  */
10
10
  export { detectFileType } from "./detect.js";
11
11
  export { signDownloadUrl, verifyDownloadUrl, } from "./sign.js";
package/dist/sign.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Short-TTL **signed download URLs** — serve an uploaded file back via a tamper-evident, expiring URL
2
+ * Short-TTL **signed download URLs** - serve an uploaded file back via a tamper-evident, expiring URL
3
3
  * (HMAC-SHA256 over the path + query + expiry). WebCrypto only (portable across Bun/Node/Deno/workerd,
4
4
  * no `node:crypto`). Verification is constant-time (`crypto.subtle.verify`). Sign a *relative*
5
5
  * URL/path; the returned string is relative so it works behind any host. `vexp` / `vsig` are reserved
package/dist/sign.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Short-TTL **signed download URLs** — serve an uploaded file back via a tamper-evident, expiring URL
2
+ * Short-TTL **signed download URLs** - serve an uploaded file back via a tamper-evident, expiring URL
3
3
  * (HMAC-SHA256 over the path + query + expiry). WebCrypto only (portable across Bun/Node/Deno/workerd,
4
4
  * no `node:crypto`). Verification is constant-time (`crypto.subtle.verify`). Sign a *relative*
5
5
  * URL/path; the returned string is relative so it works behind any host. `vexp` / `vsig` are reserved
@@ -7,7 +7,7 @@
7
7
  */
8
8
  const ENC = new TextEncoder();
9
9
  const DUMMY_BASE = "http://localhost"; // lets us parse a relative path/URL with the WHATWG URL parser
10
- // 256-bit secret floor — mirrors @nifrajs/core's HMAC guard. This package is deliberately
10
+ // 256-bit secret floor - mirrors @nifrajs/core's HMAC guard. This package is deliberately
11
11
  // dependency-free (ships its own crypto), so the constant is inlined rather than imported.
12
12
  const MIN_SECRET_BYTES = 32;
13
13
  const importKey = (secret) => {
package/dist/strip.d.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Strip EXIF/GPS/metadata from an uploaded image by **re-encoding** it — decoders don't carry
2
+ * Strip EXIF/GPS/metadata from an uploaded image by **re-encoding** it - decoders don't carry
3
3
  * embedded metadata through, so a decode→encode round-trip drops it (and normalizes the format).
4
4
  *
5
5
  * Backend-agnostic: pass any `@nifrajs/image` backend (`bunImageBackend`, `sharpImageBackend`,
6
- * `wasmImageBackend`) — the structural type below matches them, so this package takes **no** dependency
6
+ * `wasmImageBackend`) - the structural type below matches them, so this package takes **no** dependency
7
7
  * on `@nifrajs/image`. Run server-side (the codecs are Node/Bun-only); validate the upload first
8
8
  * (`validateUpload`) so you only re-encode real images.
9
9
  */
10
10
  type StripFormat = "webp" | "jpeg" | "png";
11
- /** The slice of `@nifrajs/image`'s `ImageBackend` this needs — `probe` for dims/format, `transform` to re-encode. */
11
+ /** The slice of `@nifrajs/image`'s `ImageBackend` this needs - `probe` for dims/format, `transform` to re-encode. */
12
12
  export interface ImageReencoder {
13
13
  probe(bytes: Uint8Array): Promise<{
14
14
  readonly width: number;
@@ -27,7 +27,7 @@ export interface ImageReencoder {
27
27
  export interface StripImageMetadataOptions {
28
28
  /** Output format. Default: keep png/jpeg, else re-encode to webp. */
29
29
  readonly format?: StripFormat;
30
- /** Encode quality (1–100). Default 82. */
30
+ /** Encode quality (1-100). Default 82. */
31
31
  readonly quality?: number;
32
32
  }
33
33
  /** Re-encode an image to its intrinsic size, dropping all embedded metadata. Returns clean bytes. */
package/dist/strip.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Strip EXIF/GPS/metadata from an uploaded image by **re-encoding** it — decoders don't carry
2
+ * Strip EXIF/GPS/metadata from an uploaded image by **re-encoding** it - decoders don't carry
3
3
  * embedded metadata through, so a decode→encode round-trip drops it (and normalizes the format).
4
4
  *
5
5
  * Backend-agnostic: pass any `@nifrajs/image` backend (`bunImageBackend`, `sharpImageBackend`,
6
- * `wasmImageBackend`) — the structural type below matches them, so this package takes **no** dependency
6
+ * `wasmImageBackend`) - the structural type below matches them, so this package takes **no** dependency
7
7
  * on `@nifrajs/image`. Run server-side (the codecs are Node/Bun-only); validate the upload first
8
8
  * (`validateUpload`) so you only re-encode real images.
9
9
  */
@@ -18,7 +18,7 @@ export type UploadResult = {
18
18
  export interface ValidateUploadOptions {
19
19
  /** Max byte length. A `Blob` over this is rejected without reading it. */
20
20
  readonly maxBytes: number;
21
- /** Allowed MIME types — exact (`"image/png"`) or a `image/*` wildcard. Omit ⇒ any recognized type. */
21
+ /** Allowed MIME types - exact (`"image/png"`) or a `image/*` wildcard. Omit ⇒ any recognized type. */
22
22
  readonly accept?: readonly string[];
23
23
  }
24
24
  /** Validate uploaded bytes/Blob: size cap + magic-byte type sniff against an optional allow-list. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nifrajs/uploads",
3
- "version": "2.2.0",
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.",
3
+ "version": "2.3.0",
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",
7
7
  "typescript",
package/src/detect.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Magic-byte file-type detection — trust the bytes, not the `Content-Type` header (which a client
2
+ * Magic-byte file-type detection - trust the bytes, not the `Content-Type` header (which a client
3
3
  * sets freely). Reads only the leading bytes; dependency-free + edge-safe. Covers the common upload
4
4
  * types; returns `null` for anything unrecognized (incl. text formats like SVG/CSV that have no magic
5
- * number — handle those explicitly if you accept them).
5
+ * number - handle those explicitly if you accept them).
6
6
  */
7
7
 
8
8
  export interface FileType {
@@ -23,19 +23,19 @@ export function detectFileType(bytes: Uint8Array): FileType | null {
23
23
  return { mime: "image/png", ext: "png" }
24
24
  if (at(0, 0x47, 0x49, 0x46, 0x38)) return { mime: "image/gif", ext: "gif" }
25
25
 
26
- // RIFF container (bytes 0–3 "RIFF", 8–11 the form type)
26
+ // RIFF container (bytes 0-3 "RIFF", 8-11 the form type)
27
27
  if (at(0, 0x52, 0x49, 0x46, 0x46)) {
28
28
  if (at(8, 0x57, 0x45, 0x42, 0x50)) return { mime: "image/webp", ext: "webp" }
29
29
  if (at(8, 0x57, 0x41, 0x56, 0x45)) return { mime: "audio/wav", ext: "wav" }
30
30
  if (at(8, 0x41, 0x56, 0x49, 0x20)) return { mime: "video/x-msvideo", ext: "avi" }
31
31
  }
32
32
 
33
- // ISO-BMFF (`ftyp` box at offset 4) — mp4 / avif / heic / m4a, disambiguated by the major brand.
33
+ // ISO-BMFF (`ftyp` box at offset 4) - mp4 / avif / heic / m4a, disambiguated by the major brand.
34
34
  if (at(4, 0x66, 0x74, 0x79, 0x70)) {
35
35
  const brand = String.fromCharCode(bytes[8] ?? 0, bytes[9] ?? 0, bytes[10] ?? 0, bytes[11] ?? 0)
36
36
  if (brand === "avif" || brand === "avis") return { mime: "image/avif", ext: "avif" }
37
37
  if (brand.startsWith("hei") || brand === "mif1") return { mime: "image/heic", ext: "heic" }
38
- // Audio brands share the ISO-BMFF container but are NOT video — labeling them `video/mp4` would
38
+ // Audio brands share the ISO-BMFF container but are NOT video - labeling them `video/mp4` would
39
39
  // both reject real audio under an `audio/*` allow-list and admit it under `video/*`. `M4A `/`M4B `
40
40
  // are AAC audio / audiobooks.
41
41
  if (brand === "M4A " || brand === "M4B ") return { mime: "audio/mp4", ext: "m4a" }
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * `@nifrajs/uploads` — file-upload hardening, dependency-free + edge-safe.
2
+ * `@nifrajs/uploads` - file-upload hardening, dependency-free + edge-safe.
3
3
  *
4
- * - {@link detectFileType} — sniff the real type from magic bytes (not the client's `Content-Type`).
5
- * - {@link validateUpload} — size cap + magic-byte type allow-list. Pair with `@nifrajs/core`'s
4
+ * - {@link detectFileType} - sniff the real type from magic bytes (not the client's `Content-Type`).
5
+ * - {@link validateUpload} - size cap + magic-byte type allow-list. Pair with `@nifrajs/core`'s
6
6
  * `c.boundedBody(maxBytes)` to also bound the read.
7
- * - {@link signDownloadUrl} / {@link verifyDownloadUrl} — short-TTL, tamper-evident download URLs.
8
- * - {@link stripImageMetadata} — drop EXIF/GPS by re-encoding via any `@nifrajs/image` backend.
7
+ * - {@link signDownloadUrl} / {@link verifyDownloadUrl} - short-TTL, tamper-evident download URLs.
8
+ * - {@link stripImageMetadata} - drop EXIF/GPS by re-encoding via any `@nifrajs/image` backend.
9
9
  */
10
10
  export { detectFileType, type FileType } from "./detect.ts"
11
11
  export {
package/src/sign.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Short-TTL **signed download URLs** — serve an uploaded file back via a tamper-evident, expiring URL
2
+ * Short-TTL **signed download URLs** - serve an uploaded file back via a tamper-evident, expiring URL
3
3
  * (HMAC-SHA256 over the path + query + expiry). WebCrypto only (portable across Bun/Node/Deno/workerd,
4
4
  * no `node:crypto`). Verification is constant-time (`crypto.subtle.verify`). Sign a *relative*
5
5
  * URL/path; the returned string is relative so it works behind any host. `vexp` / `vsig` are reserved
@@ -9,7 +9,7 @@
9
9
  const ENC = new TextEncoder()
10
10
  const DUMMY_BASE = "http://localhost" // lets us parse a relative path/URL with the WHATWG URL parser
11
11
 
12
- // 256-bit secret floor — mirrors @nifrajs/core's HMAC guard. This package is deliberately
12
+ // 256-bit secret floor - mirrors @nifrajs/core's HMAC guard. This package is deliberately
13
13
  // dependency-free (ships its own crypto), so the constant is inlined rather than imported.
14
14
  const MIN_SECRET_BYTES = 32
15
15
 
package/src/strip.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Strip EXIF/GPS/metadata from an uploaded image by **re-encoding** it — decoders don't carry
2
+ * Strip EXIF/GPS/metadata from an uploaded image by **re-encoding** it - decoders don't carry
3
3
  * embedded metadata through, so a decode→encode round-trip drops it (and normalizes the format).
4
4
  *
5
5
  * Backend-agnostic: pass any `@nifrajs/image` backend (`bunImageBackend`, `sharpImageBackend`,
6
- * `wasmImageBackend`) — the structural type below matches them, so this package takes **no** dependency
6
+ * `wasmImageBackend`) - the structural type below matches them, so this package takes **no** dependency
7
7
  * on `@nifrajs/image`. Run server-side (the codecs are Node/Bun-only); validate the upload first
8
8
  * (`validateUpload`) so you only re-encode real images.
9
9
  */
10
10
 
11
11
  type StripFormat = "webp" | "jpeg" | "png"
12
12
 
13
- /** The slice of `@nifrajs/image`'s `ImageBackend` this needs — `probe` for dims/format, `transform` to re-encode. */
13
+ /** The slice of `@nifrajs/image`'s `ImageBackend` this needs - `probe` for dims/format, `transform` to re-encode. */
14
14
  export interface ImageReencoder {
15
15
  probe(
16
16
  bytes: Uint8Array,
@@ -26,7 +26,7 @@ export interface ImageReencoder {
26
26
  export interface StripImageMetadataOptions {
27
27
  /** Output format. Default: keep png/jpeg, else re-encode to webp. */
28
28
  readonly format?: StripFormat
29
- /** Encode quality (1–100). Default 82. */
29
+ /** Encode quality (1-100). Default 82. */
30
30
  readonly quality?: number
31
31
  }
32
32
 
package/src/validate.ts CHANGED
@@ -18,7 +18,7 @@ export type UploadResult =
18
18
  export interface ValidateUploadOptions {
19
19
  /** Max byte length. A `Blob` over this is rejected without reading it. */
20
20
  readonly maxBytes: number
21
- /** Allowed MIME types — exact (`"image/png"`) or a `image/*` wildcard. Omit ⇒ any recognized type. */
21
+ /** Allowed MIME types - exact (`"image/png"`) or a `image/*` wildcard. Omit ⇒ any recognized type. */
22
22
  readonly accept?: readonly string[]
23
23
  }
24
24