@nifrajs/image 2.2.0 → 2.4.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/image
2
2
 
3
- Image optimization for nifra — a CLS-safe responsive <Image> + pluggable loaders (CDN or self-hosted) + pure-JS dimension reading. Zero-dependency core; optional Bun.Image-backed resize endpoint at @nifrajs/image/server.
3
+ Image optimization for nifra - a CLS-safe responsive <Image> + pluggable loaders (CDN or self-hosted) + pure-JS dimension reading. Zero-dependency core; optional Bun.Image-backed resize endpoint at @nifrajs/image/server.
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/backend.d.ts CHANGED
@@ -1,21 +1,21 @@
1
1
  /**
2
- * `@nifrajs/image/backends` — the codec seam + the official {@link ImageBackend} implementations, kept
2
+ * `@nifrajs/image/backends` - the codec seam + the official {@link ImageBackend} implementations, kept
3
3
  * **dependency-free and edge-safe** (no `node:` imports), so the WASM backend can run on Workers /
4
- * Vercel-Edge / Deno-Deploy. `createImageHandler` (in `@nifrajs/image/server`, Node-only — it touches the
4
+ * Vercel-Edge / Deno-Deploy. `createImageHandler` (in `@nifrajs/image/server`, Node-only - it touches the
5
5
  * filesystem) consumes this seam and owns all request-level security; a backend only
6
6
  * decodes/resizes/encodes and translates codec failures into {@link ImageProcessingError}.
7
7
  *
8
8
  * Pick a backend for your runtime:
9
- * - {@link bunImageBackend} — `Bun.Image` (libjpeg-turbo / libspng / libwebp). Bun servers.
10
- * - {@link sharpImageBackend} — pass your `sharp` import. Node servers.
11
- * - {@link wasmImageBackend} — pass WASM codecs (e.g. jSquash). Any runtime, including the edge.
9
+ * - {@link bunImageBackend} - `Bun.Image` (libjpeg-turbo / libspng / libwebp). Bun servers.
10
+ * - {@link sharpImageBackend} - pass your `sharp` import. Node servers.
11
+ * - {@link wasmImageBackend} - pass WASM codecs (e.g. jSquash). Any runtime, including the edge.
12
12
  */
13
- /** Output formats nifra's endpoint can emit. AVIF is intentionally excluded — `Bun.Image` reports
13
+ /** Output formats nifra's endpoint can emit. AVIF is intentionally excluded - `Bun.Image` reports
14
14
  * `ERR_IMAGE_FORMAT_UNSUPPORTED` for AVIF encode on common platforms, so offering it would 500. */
15
15
  export type OutputFormat = "webp" | "jpeg" | "png";
16
16
  export declare const CONTENT_TYPE: Record<OutputFormat, string>;
17
17
  /** Header-only probe of a source image: intrinsic dimensions + decoded format. Must be cheap (no full
18
- * decode) — it gates the decompression-bomb and no-upscale checks before the expensive resize. */
18
+ * decode) - it gates the decompression-bomb and no-upscale checks before the expensive resize. */
19
19
  export interface ImageProbe {
20
20
  readonly width: number;
21
21
  readonly height: number;
@@ -24,7 +24,7 @@ export interface ImageProbe {
24
24
  }
25
25
  export interface ResizeInput {
26
26
  readonly bytes: Uint8Array;
27
- /** Target width in px — already clamped to `[1, maxWidth]` **and** to the source's intrinsic width
27
+ /** Target width in px - already clamped to `[1, maxWidth]` **and** to the source's intrinsic width
28
28
  * (the handler never asks a backend to upscale). Aspect ratio is preserved. */
29
29
  readonly width: number;
30
30
  /** Encoder quality `1..100` (ignored for lossless `png`). */
@@ -59,7 +59,7 @@ export declare class ImageProcessingError extends Error {
59
59
  */
60
60
  export declare function bunImageBackend(): ImageBackend;
61
61
  /** The slice of a [sharp](https://sharp.pixelplumbing.com) instance this backend uses. Declared
62
- * structurally so `@nifrajs/image` has no dependency on sharp — pass your own `sharp` import. */
62
+ * structurally so `@nifrajs/image` has no dependency on sharp - pass your own `sharp` import. */
63
63
  export type SharpLike = (input: Uint8Array) => SharpInstance;
64
64
  interface SharpInstance {
65
65
  metadata(): Promise<{
@@ -82,7 +82,7 @@ interface SharpInstance {
82
82
  }
83
83
  /**
84
84
  * {@link ImageBackend} backed by [sharp](https://sharp.pixelplumbing.com) (libvips) for Node servers.
85
- * Pass your `sharp` import — `@nifrajs/image` never imports it, so it stays dependency-free and you control
85
+ * Pass your `sharp` import - `@nifrajs/image` never imports it, so it stays dependency-free and you control
86
86
  * the version:
87
87
  *
88
88
  * ```ts
@@ -91,7 +91,7 @@ interface SharpInstance {
91
91
  * ```
92
92
  */
93
93
  export declare function sharpImageBackend(sharp: SharpLike): ImageBackend;
94
- /** A decoded image: RGBA pixels + dimensions — the lingua franca of WASM codecs (jSquash, Photon, …). */
94
+ /** A decoded image: RGBA pixels + dimensions - the lingua franca of WASM codecs (jSquash, Photon, …). */
95
95
  export interface DecodedImage {
96
96
  /** RGBA bytes, length `width * height * 4`. */
97
97
  readonly data: Uint8Array;
@@ -99,7 +99,7 @@ export interface DecodedImage {
99
99
  readonly height: number;
100
100
  }
101
101
  /**
102
- * Pluggable WASM codec set — decode/resize/encode. Declared structurally so `@nifrajs/image` depends on no
102
+ * Pluggable WASM codec set - decode/resize/encode. Declared structurally so `@nifrajs/image` depends on no
103
103
  * WASM library; wire your own (jSquash is the common pure-WASM, edge-safe choice). The handler probes
104
104
  * dimensions from the source header (bomb-safe), so `decode` runs only inside `transform`.
105
105
  */
@@ -112,7 +112,7 @@ export interface WasmImageCodecs {
112
112
  encode(image: DecodedImage, format: OutputFormat, quality: number): Promise<Uint8Array> | Uint8Array;
113
113
  }
114
114
  /**
115
- * {@link ImageBackend} backed by injected WASM codecs — the only backend that runs on the **edge**
115
+ * {@link ImageBackend} backed by injected WASM codecs - the only backend that runs on the **edge**
116
116
  * (Workers / Vercel-Edge / Deno-Deploy), where neither `Bun.Image` nor sharp exists. `probe` reads the
117
117
  * source header via nifra's dependency-free reader (so decompression bombs are rejected before any
118
118
  * decode); `transform` decodes → resizes (aspect-preserving) → encodes through your codecs.
package/dist/backend.js CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * `@nifrajs/image/backends` — the codec seam + the official {@link ImageBackend} implementations, kept
2
+ * `@nifrajs/image/backends` - the codec seam + the official {@link ImageBackend} implementations, kept
3
3
  * **dependency-free and edge-safe** (no `node:` imports), so the WASM backend can run on Workers /
4
- * Vercel-Edge / Deno-Deploy. `createImageHandler` (in `@nifrajs/image/server`, Node-only — it touches the
4
+ * Vercel-Edge / Deno-Deploy. `createImageHandler` (in `@nifrajs/image/server`, Node-only - it touches the
5
5
  * filesystem) consumes this seam and owns all request-level security; a backend only
6
6
  * decodes/resizes/encodes and translates codec failures into {@link ImageProcessingError}.
7
7
  *
8
8
  * Pick a backend for your runtime:
9
- * - {@link bunImageBackend} — `Bun.Image` (libjpeg-turbo / libspng / libwebp). Bun servers.
10
- * - {@link sharpImageBackend} — pass your `sharp` import. Node servers.
11
- * - {@link wasmImageBackend} — pass WASM codecs (e.g. jSquash). Any runtime, including the edge.
9
+ * - {@link bunImageBackend} - `Bun.Image` (libjpeg-turbo / libspng / libwebp). Bun servers.
10
+ * - {@link sharpImageBackend} - pass your `sharp` import. Node servers.
11
+ * - {@link wasmImageBackend} - pass WASM codecs (e.g. jSquash). Any runtime, including the edge.
12
12
  */
13
13
  import { imageDimensions } from "./dimensions.js";
14
14
  export const CONTENT_TYPE = {
@@ -77,7 +77,7 @@ function toBunError(err) {
77
77
  }
78
78
  /**
79
79
  * {@link ImageBackend} backed by [sharp](https://sharp.pixelplumbing.com) (libvips) for Node servers.
80
- * Pass your `sharp` import — `@nifrajs/image` never imports it, so it stays dependency-free and you control
80
+ * Pass your `sharp` import - `@nifrajs/image` never imports it, so it stays dependency-free and you control
81
81
  * the version:
82
82
  *
83
83
  * ```ts
@@ -130,7 +130,7 @@ function toSharpError(err) {
130
130
  return new ImageProcessingError("decode", "sharp failed to process the image");
131
131
  }
132
132
  /**
133
- * {@link ImageBackend} backed by injected WASM codecs — the only backend that runs on the **edge**
133
+ * {@link ImageBackend} backed by injected WASM codecs - the only backend that runs on the **edge**
134
134
  * (Workers / Vercel-Edge / Deno-Deploy), where neither `Bun.Image` nor sharp exists. `probe` reads the
135
135
  * source header via nifra's dependency-free reader (so decompression bombs are rejected before any
136
136
  * decode); `transform` decodes → resizes (aspect-preserving) → encodes through your codecs.
@@ -144,7 +144,7 @@ function toSharpError(err) {
144
144
  export function wasmImageBackend(codecs) {
145
145
  return {
146
146
  async probe(bytes) {
147
- const info = imageDimensions(bytes); // header-only — never allocates a decoded buffer
147
+ const info = imageDimensions(bytes); // header-only - never allocates a decoded buffer
148
148
  if (info === null) {
149
149
  throw new ImageProcessingError("decode", "unrecognized image header");
150
150
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Read an image's intrinsic dimensions from its file **header**, in pure JS — no decode, no codec, no
2
+ * Read an image's intrinsic dimensions from its file **header**, in pure JS - no decode, no codec, no
3
3
  * dependency. Supports PNG, JPEG, GIF, and WebP (VP8/VP8L/VP8X). Used to give `<Image>` CLS-safe
4
4
  * `width`/`height` (build-time tooling can pre-read them into a manifest).
5
5
  */
@@ -14,7 +14,7 @@ export declare function imageDimensions(bytes: Uint8Array): ImageInfo | null;
14
14
  /**
15
15
  * Read just the leading bytes of an image file (via the platform `Bun.file`/`fetch` blob) and parse its
16
16
  * dimensions. Build-time tooling: pre-read dimensions into a manifest so `<Image>` is CLS-safe without
17
- * hardcoding sizes. Reads at most `maxBytes` (default 64 KB — enough for any header).
17
+ * hardcoding sizes. Reads at most `maxBytes` (default 64 KB - enough for any header).
18
18
  */
19
19
  export declare function readImageDimensions(source: {
20
20
  arrayBuffer(): Promise<ArrayBuffer>;
@@ -18,7 +18,7 @@ const jpeg = (b, dv) => {
18
18
  if (dv.getUint8(offset) !== 0xff)
19
19
  return null; // not aligned on a marker → malformed
20
20
  const marker = dv.getUint8(offset + 1);
21
- // SOF0..SOF15 carry the frame's height/width — except DHT(c4)/DNL(c8)/DAC(cc).
21
+ // SOF0..SOF15 carry the frame's height/width - except DHT(c4)/DNL(c8)/DAC(cc).
22
22
  if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
23
23
  return { height: dv.getUint16(offset + 5), width: dv.getUint16(offset + 7), format: "jpeg" };
24
24
  }
@@ -64,7 +64,7 @@ export function imageDimensions(bytes) {
64
64
  /**
65
65
  * Read just the leading bytes of an image file (via the platform `Bun.file`/`fetch` blob) and parse its
66
66
  * dimensions. Build-time tooling: pre-read dimensions into a manifest so `<Image>` is CLS-safe without
67
- * hardcoding sizes. Reads at most `maxBytes` (default 64 KB — enough for any header).
67
+ * hardcoding sizes. Reads at most `maxBytes` (default 64 KB - enough for any header).
68
68
  */
69
69
  export async function readImageDimensions(source, maxBytes = 65_536) {
70
70
  if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
package/dist/html.d.ts CHANGED
@@ -12,8 +12,8 @@ export interface HtmlImageAttrs {
12
12
  readonly fetchpriority?: "high";
13
13
  }
14
14
  /**
15
- * Map a {@link ResolvedImage} to plain lowercase HTML `<img>` attribute names — `srcset` (not React's
16
- * `srcSet`), `fetchpriority` (not `fetchPriority`) — dropping unset optionals. For the adapters that
15
+ * Map a {@link ResolvedImage} to plain lowercase HTML `<img>` attribute names - `srcset` (not React's
16
+ * `srcSet`), `fetchpriority` (not `fetchPriority`) - dropping unset optionals. For the adapters that
17
17
  * spread attributes straight onto a host element (Solid / Vue / Svelte / Preact); React consumes
18
18
  * `ResolvedImage` directly because it requires the camelCased prop names.
19
19
  */
package/dist/html.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Map a {@link ResolvedImage} to plain lowercase HTML `<img>` attribute names — `srcset` (not React's
3
- * `srcSet`), `fetchpriority` (not `fetchPriority`) — dropping unset optionals. For the adapters that
2
+ * Map a {@link ResolvedImage} to plain lowercase HTML `<img>` attribute names - `srcset` (not React's
3
+ * `srcSet`), `fetchpriority` (not `fetchPriority`) - dropping unset optionals. For the adapters that
4
4
  * spread attributes straight onto a host element (Solid / Vue / Svelte / Preact); React consumes
5
5
  * `ResolvedImage` directly because it requires the camelCased prop names.
6
6
  */
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@nifrajs/image` — image optimization for nifra. A CLS-safe responsive `<Image>` (per-adapter) built on
2
+ * `@nifrajs/image` - image optimization for nifra. A CLS-safe responsive `<Image>` (per-adapter) built on
3
3
  * the pure attribute builder `resolveImage`, pluggable CDN `ImageLoader`s (Cloudflare Images + an
4
4
  * identity default), and dependency-free intrinsic-dimension reading. nifra bundles no image codec; the
5
5
  * resize is delegated to the edge CDN via the loader. Per-adapter `<Image>` lives in the adapter packages.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@nifrajs/image` — image optimization for nifra. A CLS-safe responsive `<Image>` (per-adapter) built on
2
+ * `@nifrajs/image` - image optimization for nifra. A CLS-safe responsive `<Image>` (per-adapter) built on
3
3
  * the pure attribute builder `resolveImage`, pluggable CDN `ImageLoader`s (Cloudflare Images + an
4
4
  * identity default), and dependency-free intrinsic-dimension reading. nifra bundles no image codec; the
5
5
  * resize is delegated to the edge CDN via the loader. Per-adapter `<Image>` lives in the adapter packages.
package/dist/resolve.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The pure attribute builder shared by every adapter `<Image>` — turns image props into CLS-safe,
2
+ * The pure attribute builder shared by every adapter `<Image>` - turns image props into CLS-safe,
3
3
  * responsive `<img>` attributes (`src`/`srcSet`/`sizes`/`width`/`height`/`loading`/`decoding`/
4
- * `fetchpriority`). The actual resize is delegated to an {@link ImageLoader} (a URL builder) — nifra
4
+ * `fetchpriority`). The actual resize is delegated to an {@link ImageLoader} (a URL builder) - nifra
5
5
  * bundles no codec; point the loader at your image CDN.
6
6
  */
7
7
  /** Builds a variant URL for `src` at a target pixel `width` (and optional `quality`). */
@@ -10,7 +10,7 @@ export type ImageLoader = (args: {
10
10
  width: number;
11
11
  quality?: number;
12
12
  }) => string;
13
- /** Default loader: return the source unchanged (no transform). Use when there's no image CDN — you
13
+ /** Default loader: return the source unchanged (no transform). Use when there's no image CDN - you
14
14
  * still get CLS-safe sizing + lazy loading, just no responsive variants. */
15
15
  export declare const identityLoader: ImageLoader;
16
16
  export interface CloudflareLoaderOptions {
@@ -18,7 +18,7 @@ export interface CloudflareLoaderOptions {
18
18
  readonly base?: string;
19
19
  }
20
20
  /**
21
- * Cloudflare Images loader — builds `/cdn-cgi/image/<options>/<source>` URLs that the Cloudflare edge
21
+ * Cloudflare Images loader - builds `/cdn-cgi/image/<options>/<source>` URLs that the Cloudflare edge
22
22
  * resizes on the fly (also emits `format=auto` for webp/avif negotiation). Works on Cloudflare Pages /
23
23
  * Workers with Images enabled.
24
24
  */
@@ -28,9 +28,9 @@ export interface SelfHostedLoaderOptions {
28
28
  readonly endpoint: string;
29
29
  /**
30
30
  * HMAC secret for **signed URLs**. When set, each URL gets a stable `&s=` signature and the handler
31
- * must be configured with the SAME `signing.secret` — it then rejects any unsigned/forged `(src, w, q)`,
31
+ * must be configured with the SAME `signing.secret` - it then rejects any unsigned/forged `(src, w, q)`,
32
32
  * shutting down resize-bombing. ⚠️ The signer holds the secret, so a loader created with it is
33
- * **server-only** — inject it like a session secret (from `env`), never import this config into a
33
+ * **server-only** - inject it like a session secret (from `env`), never import this config into a
34
34
  * route/client module. Signatures are stable (no expiry), so SSR-signed URLs hydrate + cache identically.
35
35
  */
36
36
  readonly secret?: string;
@@ -43,13 +43,13 @@ export interface SelfHostedLoaderOptions {
43
43
  */
44
44
  export declare function selfHostedLoader(options: SelfHostedLoaderOptions): ImageLoader;
45
45
  export interface SignImageUrlOptions {
46
- /** HMAC secret — must match the handler's `signing.secret`. */
46
+ /** HMAC secret - must match the handler's `signing.secret`. */
47
47
  readonly secret: string;
48
48
  /** Seconds until the URL expires (adds `&exp=`). Omit for a stable, cacheable-forever signed URL. */
49
49
  readonly expiresIn?: number;
50
50
  }
51
51
  /**
52
- * Mint a **signed** self-hosted image URL on the server — for cases the (stable) `selfHostedLoader`
52
+ * Mint a **signed** self-hosted image URL on the server - for cases the (stable) `selfHostedLoader`
53
53
  * doesn't cover, chiefly **time-limited** access (`expiresIn`) to private images. Server-only (it holds
54
54
  * the secret). Pair with a passthrough loader, or use the signed string as a plain `src`.
55
55
  *
@@ -64,11 +64,11 @@ export declare function signImageUrl(endpoint: string, image: {
64
64
  }, options: SignImageUrlOptions): string;
65
65
  export interface ImageProps {
66
66
  readonly src: string;
67
- /** Intrinsic width (px) — **required**, reserves layout space (no CLS). */
67
+ /** Intrinsic width (px) - **required**, reserves layout space (no CLS). */
68
68
  readonly width: number;
69
- /** Intrinsic height (px) — **required** (no CLS). */
69
+ /** Intrinsic height (px) - **required** (no CLS). */
70
70
  readonly height: number;
71
- /** Alt text — **required** for accessibility (use `alt=""` for decorative images). */
71
+ /** Alt text - **required** for accessibility (use `alt=""` for decorative images). */
72
72
  readonly alt: string;
73
73
  /** `sizes` attribute for responsive selection (e.g. `"(max-width: 600px) 100vw, 600px"`). */
74
74
  readonly sizes?: string;
package/dist/resolve.js CHANGED
@@ -1,15 +1,15 @@
1
1
  /**
2
- * The pure attribute builder shared by every adapter `<Image>` — turns image props into CLS-safe,
2
+ * The pure attribute builder shared by every adapter `<Image>` - turns image props into CLS-safe,
3
3
  * responsive `<img>` attributes (`src`/`srcSet`/`sizes`/`width`/`height`/`loading`/`decoding`/
4
- * `fetchpriority`). The actual resize is delegated to an {@link ImageLoader} (a URL builder) — nifra
4
+ * `fetchpriority`). The actual resize is delegated to an {@link ImageLoader} (a URL builder) - nifra
5
5
  * bundles no codec; point the loader at your image CDN.
6
6
  */
7
7
  import { signImageParams } from "./sign.js";
8
- /** Default loader: return the source unchanged (no transform). Use when there's no image CDN — you
8
+ /** Default loader: return the source unchanged (no transform). Use when there's no image CDN - you
9
9
  * still get CLS-safe sizing + lazy loading, just no responsive variants. */
10
10
  export const identityLoader = ({ src }) => src;
11
11
  /**
12
- * Cloudflare Images loader — builds `/cdn-cgi/image/<options>/<source>` URLs that the Cloudflare edge
12
+ * Cloudflare Images loader - builds `/cdn-cgi/image/<options>/<source>` URLs that the Cloudflare edge
13
13
  * resizes on the fly (also emits `format=auto` for webp/avif negotiation). Works on Cloudflare Pages /
14
14
  * Workers with Images enabled.
15
15
  */
@@ -41,7 +41,7 @@ export function selfHostedLoader(options) {
41
41
  };
42
42
  }
43
43
  /**
44
- * Mint a **signed** self-hosted image URL on the server — for cases the (stable) `selfHostedLoader`
44
+ * Mint a **signed** self-hosted image URL on the server - for cases the (stable) `selfHostedLoader`
45
45
  * doesn't cover, chiefly **time-limited** access (`expiresIn`) to private images. Server-only (it holds
46
46
  * the secret). Pair with a passthrough loader, or use the signed string as a plain `src`.
47
47
  *
@@ -74,7 +74,7 @@ export function resolveImage(props, loader = identityLoader) {
74
74
  props.width <= 0 ||
75
75
  !Number.isFinite(props.height) ||
76
76
  props.height <= 0) {
77
- throw new Error(`[nifra/image] <Image> requires positive width + height (got ${props.width}×${props.height}) — they reserve layout space to prevent CLS.`);
77
+ throw new Error(`[nifra/image] <Image> requires positive width + height (got ${props.width}×${props.height}) - they reserve layout space to prevent CLS.`);
78
78
  }
79
79
  const eager = props.priority === true || props.loading === "eager";
80
80
  const widths = [...new Set(props.widths ?? [props.width, props.width * 2])].sort((a, b) => a - b);
@@ -85,7 +85,7 @@ export function resolveImage(props, loader = identityLoader) {
85
85
  const entries = widths.map((w) => ({ url: at(w), w }));
86
86
  const allSame = entries.every((e) => e.url === entries[0]?.url);
87
87
  const srcSet = allSame ? undefined : entries.map((e) => `${e.url} ${e.w}w`).join(", ");
88
- // `src` is the 1× (intrinsic-width) variant — the fallback for browsers ignoring srcSet.
88
+ // `src` is the 1× (intrinsic-width) variant - the fallback for browsers ignoring srcSet.
89
89
  const base = {
90
90
  src: at(props.width),
91
91
  width: props.width,
package/dist/server.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  /**
2
- * `@nifrajs/image/server` — nifra's **self-hosted** image resize endpoint. `createImageHandler` returns a
2
+ * `@nifrajs/image/server` - nifra's **self-hosted** image resize endpoint. `createImageHandler` returns a
3
3
  * `(req: Request) => Promise<Response>` you mount at the path your {@link selfHostedLoader} points to
4
4
  * (e.g. `"/_image"`). It validates the query, safely resolves the source (local file under a `root`, or
5
- * a remote URL on an `allowedOrigins` allowlist — both **fail-closed** against SSRF), then decodes →
5
+ * a remote URL on an `allowedOrigins` allowlist - both **fail-closed** against SSRF), then decodes →
6
6
  * resizes (never upscaling) → re-encodes via a pluggable {@link ImageBackend}. The default
7
7
  * {@link bunImageBackend} uses `Bun.Image` (libjpeg-turbo / libspng / libwebp, off-thread).
8
8
  *
9
9
  * This subpath touches the filesystem and a native codec, so it is for **Node/Bun servers**, not the
10
- * edge. On Workers / Vercel-Edge / Deno-Deploy there is no native codec — use the CDN `cloudflareLoader`
10
+ * edge. On Workers / Vercel-Edge / Deno-Deploy there is no native codec - use the CDN `cloudflareLoader`
11
11
  * from `@nifrajs/image` instead. The dependency-free core (`@nifrajs/image`) never imports this module.
12
12
  */
13
13
  import { type ImageBackend } from "./backend.js";
@@ -23,7 +23,7 @@ export interface ImageHandlerOptions {
23
23
  readonly allowedOrigins?: readonly string[];
24
24
  /** Max bytes read from a source before rejecting (413). Default 20 MiB. */
25
25
  readonly maxSourceBytes?: number;
26
- /** Max source pixels (w×h) before rejecting (413) — decompression-bomb guard. Default 40 MP. */
26
+ /** Max source pixels (w×h) before rejecting (413) - decompression-bomb guard. Default 40 MP. */
27
27
  readonly maxSourcePixels?: number;
28
28
  /** Hard cap on the requested width; larger `?w` is clamped down. Default 3840. */
29
29
  readonly maxWidth?: number;
@@ -40,7 +40,7 @@ export interface ImageHandlerOptions {
40
40
  readonly fetch?: typeof fetch;
41
41
  /** Require **signed URLs**: every request must carry a valid `&s=` HMAC over `(src, w, q[, exp])` or
42
42
  * it's rejected with `403`. Use the SAME `secret` as your `selfHostedLoader`/`signImageUrl`. This
43
- * locks the endpoint to URLs your app minted — the defense against resize-bombing. */
43
+ * locks the endpoint to URLs your app minted - the defense against resize-bombing. */
44
44
  readonly signing?: {
45
45
  readonly secret: string;
46
46
  };
package/dist/server.js CHANGED
@@ -1,13 +1,13 @@
1
1
  /**
2
- * `@nifrajs/image/server` — nifra's **self-hosted** image resize endpoint. `createImageHandler` returns a
2
+ * `@nifrajs/image/server` - nifra's **self-hosted** image resize endpoint. `createImageHandler` returns a
3
3
  * `(req: Request) => Promise<Response>` you mount at the path your {@link selfHostedLoader} points to
4
4
  * (e.g. `"/_image"`). It validates the query, safely resolves the source (local file under a `root`, or
5
- * a remote URL on an `allowedOrigins` allowlist — both **fail-closed** against SSRF), then decodes →
5
+ * a remote URL on an `allowedOrigins` allowlist - both **fail-closed** against SSRF), then decodes →
6
6
  * resizes (never upscaling) → re-encodes via a pluggable {@link ImageBackend}. The default
7
7
  * {@link bunImageBackend} uses `Bun.Image` (libjpeg-turbo / libspng / libwebp, off-thread).
8
8
  *
9
9
  * This subpath touches the filesystem and a native codec, so it is for **Node/Bun servers**, not the
10
- * edge. On Workers / Vercel-Edge / Deno-Deploy there is no native codec — use the CDN `cloudflareLoader`
10
+ * edge. On Workers / Vercel-Edge / Deno-Deploy there is no native codec - use the CDN `cloudflareLoader`
11
11
  * from `@nifrajs/image` instead. The dependency-free core (`@nifrajs/image`) never imports this module.
12
12
  */
13
13
  import { readFile, realpath, stat } from "node:fs/promises";
@@ -15,7 +15,7 @@ import { resolve as resolvePath, sep } from "node:path";
15
15
  import { bunImageBackend, ImageProcessingError, } from "./backend.js";
16
16
  import { verifyImageParams } from "./sign.js";
17
17
  // The codec seam + every official backend (Bun/sharp/WASM) live in the edge-safe `./backend.ts`
18
- // (`@nifrajs/image/backends`), importable WITHOUT this module's `node:fs` — so a WASM backend can ship to
18
+ // (`@nifrajs/image/backends`), importable WITHOUT this module's `node:fs` - so a WASM backend can ship to
19
19
  // the edge. Re-export them so `@nifrajs/image/server` stays the single import for Node servers.
20
20
  export * from "./backend.js";
21
21
  const MAX_SRC_LEN = 2048;
@@ -51,11 +51,11 @@ function resolveConfig(options) {
51
51
  };
52
52
  }
53
53
  async function handle(req, cfg) {
54
- // 1. Method — only safe reads. (HEAD shares the GET path; the body is stripped at the end.)
54
+ // 1. Method - only safe reads. (HEAD shares the GET path; the body is stripped at the end.)
55
55
  if (req.method !== "GET" && req.method !== "HEAD") {
56
56
  return errorResponse(405, "method_not_allowed", { Allow: "GET, HEAD" });
57
57
  }
58
- // 2. Validate the query at the trust boundary (strict scalar parsing — no Number() coercion).
58
+ // 2. Validate the query at the trust boundary (strict scalar parsing - no Number() coercion).
59
59
  const url = new URL(req.url);
60
60
  const src = url.searchParams.get("src");
61
61
  if (src === null || src.length === 0 || src.length > MAX_SRC_LEN) {
@@ -75,7 +75,7 @@ async function handle(req, cfg) {
75
75
  const width = Math.min(cfg.maxWidth, requestedWidth);
76
76
  const wantsWebp = (req.headers.get("accept") ?? "").includes("image/webp");
77
77
  // 2b. Signed-URL enforcement (when configured): reject any request we didn't mint. Verifies the
78
- // `&s=` HMAC over the raw (src, w, q[, exp]) — exactly what the loader/`signImageUrl` signed —
78
+ // `&s=` HMAC over the raw (src, w, q[, exp]) - exactly what the loader/`signImageUrl` signed -
79
79
  // and the expiry. Done before the ETag/fetch so unsigned/forged requests cost nothing.
80
80
  if (cfg.signing !== null) {
81
81
  const sig = url.searchParams.get("s");
@@ -136,7 +136,7 @@ async function handle(req, cfg) {
136
136
  return errorResponse(413, "source_too_large");
137
137
  return errorResponse(415, "unsupported_media_type"); // decode | unsupported
138
138
  }
139
- // Unexpected — never leak internals to the client.
139
+ // Unexpected - never leak internals to the client.
140
140
  return errorResponse(500, "internal_error");
141
141
  }
142
142
  finally {
@@ -165,7 +165,7 @@ async function fetchRemote(url, cfg) {
165
165
  let res;
166
166
  try {
167
167
  res = await cfg.fetchImpl(url, {
168
- redirect: "error", // a redirect could bounce to a disallowed origin — refuse it.
168
+ redirect: "error", // a redirect could bounce to a disallowed origin - refuse it.
169
169
  signal: AbortSignal.timeout(cfg.fetchTimeoutMs),
170
170
  headers: { Accept: "image/*" },
171
171
  });
@@ -186,7 +186,7 @@ async function fetchRemote(url, cfg) {
186
186
  async function readLocal(src, cfg) {
187
187
  if (cfg.root === null)
188
188
  return { ok: false, status: 403, code: "source_not_allowed" };
189
- // `src` is already percent-decoded by URLSearchParams — do NOT decode again (double-decode bypass).
189
+ // `src` is already percent-decoded by URLSearchParams - do NOT decode again (double-decode bypass).
190
190
  if (src.includes("\0"))
191
191
  return { ok: false, status: 400, code: "invalid_src" };
192
192
  // Strip leading slashes so a site-absolute ("/hero.jpg") or protocol-relative ("//evil") path is
@@ -207,7 +207,7 @@ async function readLocal(src, cfg) {
207
207
  return { ok: false, status: 404, code: "source_not_found" };
208
208
  if (info.size > cfg.maxSourceBytes)
209
209
  return { ok: false, status: 413, code: "source_too_large" };
210
- // Defense-in-depth: a symlink inside root could point outside it — re-check the real path.
210
+ // Defense-in-depth: a symlink inside root could point outside it - re-check the real path.
211
211
  try {
212
212
  const real = await realpath(resolved);
213
213
  const realRoot = await realpath(cfg.root);
@@ -221,7 +221,7 @@ async function readLocal(src, cfg) {
221
221
  return { ok: true, bytes: new Uint8Array(await readFile(resolved)) };
222
222
  }
223
223
  /** Read a response body, aborting (→ null) once `limit` bytes are exceeded. Trusts neither the
224
- * `Content-Length` header (checked as a fast reject) nor an absent one — the running total is the gate. */
224
+ * `Content-Length` header (checked as a fast reject) nor an absent one - the running total is the gate. */
225
225
  async function readBoundedBytes(res, limit) {
226
226
  const declared = res.headers.get("content-length");
227
227
  if (declared !== null && /^\d+$/.test(declared) && Number(declared) > limit)
@@ -302,7 +302,7 @@ function errorResponse(status, code, extraHeaders = {}) {
302
302
  });
303
303
  }
304
304
  /**
305
- * Async counting semaphore with direct slot hand-off — a released slot is passed straight to the next
305
+ * Async counting semaphore with direct slot hand-off - a released slot is passed straight to the next
306
306
  * waiter (the active count is never transiently decremented), so a concurrent `acquire()` can't slip
307
307
  * past the limit in the microtask gap. Bounds concurrent codec work.
308
308
  */
package/dist/sign.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Signed image URLs — a portable, **synchronous** HMAC-SHA256 so the (sync) `selfHostedLoader` can sign
2
+ * Signed image URLs - a portable, **synchronous** HMAC-SHA256 so the (sync) `selfHostedLoader` can sign
3
3
  * URLs inline, on any runtime including the edge (WebCrypto's HMAC is async-only; `node:crypto` isn't on
4
- * Workers). A signed URL lets `createImageHandler` reject any `(src, w, q)` it didn't authorize —
4
+ * Workers). A signed URL lets `createImageHandler` reject any `(src, w, q)` it didn't authorize -
5
5
  * shutting down resize-bombing (width/quality enumeration) and locking the endpoint to your own images.
6
6
  *
7
7
  * Pure JS, dependency-free, KAT-tested against the SHA-256 / RFC 4231 HMAC vectors. The secret never
package/dist/sign.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Signed image URLs — a portable, **synchronous** HMAC-SHA256 so the (sync) `selfHostedLoader` can sign
2
+ * Signed image URLs - a portable, **synchronous** HMAC-SHA256 so the (sync) `selfHostedLoader` can sign
3
3
  * URLs inline, on any runtime including the edge (WebCrypto's HMAC is async-only; `node:crypto` isn't on
4
- * Workers). A signed URL lets `createImageHandler` reject any `(src, w, q)` it didn't authorize —
4
+ * Workers). A signed URL lets `createImageHandler` reject any `(src, w, q)` it didn't authorize -
5
5
  * shutting down resize-bombing (width/quality enumeration) and locking the endpoint to your own images.
6
6
  *
7
7
  * Pure JS, dependency-free, KAT-tested against the SHA-256 / RFC 4231 HMAC vectors. The secret never
@@ -117,14 +117,14 @@ const hex = (bytes) => {
117
117
  export function hmacSha256Hex(key, message) {
118
118
  return hex(hmacSha256(encoder.encode(key), encoder.encode(message)));
119
119
  }
120
- /** base64url (RFC 4648 §5, unpadded) of a byte array — URL-safe, compact (43 chars for a 32-byte tag). */
120
+ /** base64url (RFC 4648 §5, unpadded) of a byte array - URL-safe, compact (43 chars for a 32-byte tag). */
121
121
  function base64url(bytes) {
122
122
  let binary = "";
123
123
  for (const b of bytes)
124
124
  binary += String.fromCharCode(b);
125
125
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
126
126
  }
127
- /** Constant-time string compare — length-independent of where the first difference is. */
127
+ /** Constant-time string compare - length-independent of where the first difference is. */
128
128
  function safeEqual(a, b) {
129
129
  if (a.length !== b.length)
130
130
  return false;
@@ -134,7 +134,7 @@ function safeEqual(a, b) {
134
134
  return diff === 0;
135
135
  }
136
136
  const canonical = (p) => `${p.src}\n${p.w}\n${p.q ?? ""}\n${p.exp ?? ""}`;
137
- // 256-bit secret floor — mirrors @nifrajs/core's HMAC guard. This package is dependency-free (ships
137
+ // 256-bit secret floor - mirrors @nifrajs/core's HMAC guard. This package is dependency-free (ships
138
138
  // its own SHA-256), so the constant is inlined rather than imported.
139
139
  const MIN_SECRET_BYTES = 32;
140
140
  /** Compute the URL signature (base64url HMAC-SHA256) for a set of image params. Guards (and so does
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nifrajs/image",
3
- "version": "2.2.0",
4
- "description": "Image optimization for nifra — a CLS-safe responsive <Image> + pluggable loaders (CDN or self-hosted) + pure-JS dimension reading. Zero-dependency core; optional Bun.Image-backed resize endpoint at @nifrajs/image/server.",
3
+ "version": "2.4.0",
4
+ "description": "Image optimization for nifra - a CLS-safe responsive <Image> + pluggable loaders (CDN or self-hosted) + pure-JS dimension reading. Zero-dependency core; optional Bun.Image-backed resize endpoint at @nifrajs/image/server.",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "typescript",
package/src/backend.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  /**
2
- * `@nifrajs/image/backends` — the codec seam + the official {@link ImageBackend} implementations, kept
2
+ * `@nifrajs/image/backends` - the codec seam + the official {@link ImageBackend} implementations, kept
3
3
  * **dependency-free and edge-safe** (no `node:` imports), so the WASM backend can run on Workers /
4
- * Vercel-Edge / Deno-Deploy. `createImageHandler` (in `@nifrajs/image/server`, Node-only — it touches the
4
+ * Vercel-Edge / Deno-Deploy. `createImageHandler` (in `@nifrajs/image/server`, Node-only - it touches the
5
5
  * filesystem) consumes this seam and owns all request-level security; a backend only
6
6
  * decodes/resizes/encodes and translates codec failures into {@link ImageProcessingError}.
7
7
  *
8
8
  * Pick a backend for your runtime:
9
- * - {@link bunImageBackend} — `Bun.Image` (libjpeg-turbo / libspng / libwebp). Bun servers.
10
- * - {@link sharpImageBackend} — pass your `sharp` import. Node servers.
11
- * - {@link wasmImageBackend} — pass WASM codecs (e.g. jSquash). Any runtime, including the edge.
9
+ * - {@link bunImageBackend} - `Bun.Image` (libjpeg-turbo / libspng / libwebp). Bun servers.
10
+ * - {@link sharpImageBackend} - pass your `sharp` import. Node servers.
11
+ * - {@link wasmImageBackend} - pass WASM codecs (e.g. jSquash). Any runtime, including the edge.
12
12
  */
13
13
 
14
14
  import { imageDimensions } from "./dimensions.ts"
15
15
 
16
- /** Output formats nifra's endpoint can emit. AVIF is intentionally excluded — `Bun.Image` reports
16
+ /** Output formats nifra's endpoint can emit. AVIF is intentionally excluded - `Bun.Image` reports
17
17
  * `ERR_IMAGE_FORMAT_UNSUPPORTED` for AVIF encode on common platforms, so offering it would 500. */
18
18
  export type OutputFormat = "webp" | "jpeg" | "png"
19
19
 
@@ -24,7 +24,7 @@ export const CONTENT_TYPE: Record<OutputFormat, string> = {
24
24
  }
25
25
 
26
26
  /** Header-only probe of a source image: intrinsic dimensions + decoded format. Must be cheap (no full
27
- * decode) — it gates the decompression-bomb and no-upscale checks before the expensive resize. */
27
+ * decode) - it gates the decompression-bomb and no-upscale checks before the expensive resize. */
28
28
  export interface ImageProbe {
29
29
  readonly width: number
30
30
  readonly height: number
@@ -34,7 +34,7 @@ export interface ImageProbe {
34
34
 
35
35
  export interface ResizeInput {
36
36
  readonly bytes: Uint8Array
37
- /** Target width in px — already clamped to `[1, maxWidth]` **and** to the source's intrinsic width
37
+ /** Target width in px - already clamped to `[1, maxWidth]` **and** to the source's intrinsic width
38
38
  * (the handler never asks a backend to upscale). Aspect ratio is preserved. */
39
39
  readonly width: number
40
40
  /** Encoder quality `1..100` (ignored for lossless `png`). */
@@ -141,7 +141,7 @@ function toBunError(err: unknown): ImageProcessingError {
141
141
  // --- sharp backend (Node) ----------------------------------------------------------------------------
142
142
 
143
143
  /** The slice of a [sharp](https://sharp.pixelplumbing.com) instance this backend uses. Declared
144
- * structurally so `@nifrajs/image` has no dependency on sharp — pass your own `sharp` import. */
144
+ * structurally so `@nifrajs/image` has no dependency on sharp - pass your own `sharp` import. */
145
145
  export type SharpLike = (input: Uint8Array) => SharpInstance
146
146
  interface SharpInstance {
147
147
  metadata(): Promise<{ width?: number; height?: number; format?: string }>
@@ -154,7 +154,7 @@ interface SharpInstance {
154
154
 
155
155
  /**
156
156
  * {@link ImageBackend} backed by [sharp](https://sharp.pixelplumbing.com) (libvips) for Node servers.
157
- * Pass your `sharp` import — `@nifrajs/image` never imports it, so it stays dependency-free and you control
157
+ * Pass your `sharp` import - `@nifrajs/image` never imports it, so it stays dependency-free and you control
158
158
  * the version:
159
159
  *
160
160
  * ```ts
@@ -208,7 +208,7 @@ function toSharpError(err: unknown): ImageProcessingError {
208
208
 
209
209
  // --- WASM backend (edge-portable) --------------------------------------------------------------------
210
210
 
211
- /** A decoded image: RGBA pixels + dimensions — the lingua franca of WASM codecs (jSquash, Photon, …). */
211
+ /** A decoded image: RGBA pixels + dimensions - the lingua franca of WASM codecs (jSquash, Photon, …). */
212
212
  export interface DecodedImage {
213
213
  /** RGBA bytes, length `width * height * 4`. */
214
214
  readonly data: Uint8Array
@@ -217,7 +217,7 @@ export interface DecodedImage {
217
217
  }
218
218
 
219
219
  /**
220
- * Pluggable WASM codec set — decode/resize/encode. Declared structurally so `@nifrajs/image` depends on no
220
+ * Pluggable WASM codec set - decode/resize/encode. Declared structurally so `@nifrajs/image` depends on no
221
221
  * WASM library; wire your own (jSquash is the common pure-WASM, edge-safe choice). The handler probes
222
222
  * dimensions from the source header (bomb-safe), so `decode` runs only inside `transform`.
223
223
  */
@@ -235,7 +235,7 @@ export interface WasmImageCodecs {
235
235
  }
236
236
 
237
237
  /**
238
- * {@link ImageBackend} backed by injected WASM codecs — the only backend that runs on the **edge**
238
+ * {@link ImageBackend} backed by injected WASM codecs - the only backend that runs on the **edge**
239
239
  * (Workers / Vercel-Edge / Deno-Deploy), where neither `Bun.Image` nor sharp exists. `probe` reads the
240
240
  * source header via nifra's dependency-free reader (so decompression bombs are rejected before any
241
241
  * decode); `transform` decodes → resizes (aspect-preserving) → encodes through your codecs.
@@ -249,7 +249,7 @@ export interface WasmImageCodecs {
249
249
  export function wasmImageBackend(codecs: WasmImageCodecs): ImageBackend {
250
250
  return {
251
251
  async probe(bytes) {
252
- const info = imageDimensions(bytes) // header-only — never allocates a decoded buffer
252
+ const info = imageDimensions(bytes) // header-only - never allocates a decoded buffer
253
253
  if (info === null) {
254
254
  throw new ImageProcessingError("decode", "unrecognized image header")
255
255
  }
package/src/dimensions.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Read an image's intrinsic dimensions from its file **header**, in pure JS — no decode, no codec, no
2
+ * Read an image's intrinsic dimensions from its file **header**, in pure JS - no decode, no codec, no
3
3
  * dependency. Supports PNG, JPEG, GIF, and WebP (VP8/VP8L/VP8X). Used to give `<Image>` CLS-safe
4
4
  * `width`/`height` (build-time tooling can pre-read them into a manifest).
5
5
  */
@@ -29,7 +29,7 @@ const jpeg = (b: Uint8Array, dv: DataView): ImageInfo | null => {
29
29
  while (offset + 9 < b.length) {
30
30
  if (dv.getUint8(offset) !== 0xff) return null // not aligned on a marker → malformed
31
31
  const marker = dv.getUint8(offset + 1)
32
- // SOF0..SOF15 carry the frame's height/width — except DHT(c4)/DNL(c8)/DAC(cc).
32
+ // SOF0..SOF15 carry the frame's height/width - except DHT(c4)/DNL(c8)/DAC(cc).
33
33
  if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
34
34
  return { height: dv.getUint16(offset + 5), width: dv.getUint16(offset + 7), format: "jpeg" }
35
35
  }
@@ -80,7 +80,7 @@ export function imageDimensions(bytes: Uint8Array): ImageInfo | null {
80
80
  /**
81
81
  * Read just the leading bytes of an image file (via the platform `Bun.file`/`fetch` blob) and parse its
82
82
  * dimensions. Build-time tooling: pre-read dimensions into a manifest so `<Image>` is CLS-safe without
83
- * hardcoding sizes. Reads at most `maxBytes` (default 64 KB — enough for any header).
83
+ * hardcoding sizes. Reads at most `maxBytes` (default 64 KB - enough for any header).
84
84
  */
85
85
  export async function readImageDimensions(
86
86
  source: { arrayBuffer(): Promise<ArrayBuffer>; stream?: () => ReadableStream<Uint8Array> },
package/src/html.ts CHANGED
@@ -14,8 +14,8 @@ export interface HtmlImageAttrs {
14
14
  }
15
15
 
16
16
  /**
17
- * Map a {@link ResolvedImage} to plain lowercase HTML `<img>` attribute names — `srcset` (not React's
18
- * `srcSet`), `fetchpriority` (not `fetchPriority`) — dropping unset optionals. For the adapters that
17
+ * Map a {@link ResolvedImage} to plain lowercase HTML `<img>` attribute names - `srcset` (not React's
18
+ * `srcSet`), `fetchpriority` (not `fetchPriority`) - dropping unset optionals. For the adapters that
19
19
  * spread attributes straight onto a host element (Solid / Vue / Svelte / Preact); React consumes
20
20
  * `ResolvedImage` directly because it requires the camelCased prop names.
21
21
  */
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@nifrajs/image` — image optimization for nifra. A CLS-safe responsive `<Image>` (per-adapter) built on
2
+ * `@nifrajs/image` - image optimization for nifra. A CLS-safe responsive `<Image>` (per-adapter) built on
3
3
  * the pure attribute builder `resolveImage`, pluggable CDN `ImageLoader`s (Cloudflare Images + an
4
4
  * identity default), and dependency-free intrinsic-dimension reading. nifra bundles no image codec; the
5
5
  * resize is delegated to the edge CDN via the loader. Per-adapter `<Image>` lives in the adapter packages.
package/src/resolve.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * The pure attribute builder shared by every adapter `<Image>` — turns image props into CLS-safe,
2
+ * The pure attribute builder shared by every adapter `<Image>` - turns image props into CLS-safe,
3
3
  * responsive `<img>` attributes (`src`/`srcSet`/`sizes`/`width`/`height`/`loading`/`decoding`/
4
- * `fetchpriority`). The actual resize is delegated to an {@link ImageLoader} (a URL builder) — nifra
4
+ * `fetchpriority`). The actual resize is delegated to an {@link ImageLoader} (a URL builder) - nifra
5
5
  * bundles no codec; point the loader at your image CDN.
6
6
  */
7
7
 
@@ -10,7 +10,7 @@ import { signImageParams } from "./sign.ts"
10
10
  /** Builds a variant URL for `src` at a target pixel `width` (and optional `quality`). */
11
11
  export type ImageLoader = (args: { src: string; width: number; quality?: number }) => string
12
12
 
13
- /** Default loader: return the source unchanged (no transform). Use when there's no image CDN — you
13
+ /** Default loader: return the source unchanged (no transform). Use when there's no image CDN - you
14
14
  * still get CLS-safe sizing + lazy loading, just no responsive variants. */
15
15
  export const identityLoader: ImageLoader = ({ src }) => src
16
16
 
@@ -20,7 +20,7 @@ export interface CloudflareLoaderOptions {
20
20
  }
21
21
 
22
22
  /**
23
- * Cloudflare Images loader — builds `/cdn-cgi/image/<options>/<source>` URLs that the Cloudflare edge
23
+ * Cloudflare Images loader - builds `/cdn-cgi/image/<options>/<source>` URLs that the Cloudflare edge
24
24
  * resizes on the fly (also emits `format=auto` for webp/avif negotiation). Works on Cloudflare Pages /
25
25
  * Workers with Images enabled.
26
26
  */
@@ -38,9 +38,9 @@ export interface SelfHostedLoaderOptions {
38
38
  readonly endpoint: string
39
39
  /**
40
40
  * HMAC secret for **signed URLs**. When set, each URL gets a stable `&s=` signature and the handler
41
- * must be configured with the SAME `signing.secret` — it then rejects any unsigned/forged `(src, w, q)`,
41
+ * must be configured with the SAME `signing.secret` - it then rejects any unsigned/forged `(src, w, q)`,
42
42
  * shutting down resize-bombing. ⚠️ The signer holds the secret, so a loader created with it is
43
- * **server-only** — inject it like a session secret (from `env`), never import this config into a
43
+ * **server-only** - inject it like a session secret (from `env`), never import this config into a
44
44
  * route/client module. Signatures are stable (no expiry), so SSR-signed URLs hydrate + cache identically.
45
45
  */
46
46
  readonly secret?: string
@@ -65,14 +65,14 @@ export function selfHostedLoader(options: SelfHostedLoaderOptions): ImageLoader
65
65
  }
66
66
 
67
67
  export interface SignImageUrlOptions {
68
- /** HMAC secret — must match the handler's `signing.secret`. */
68
+ /** HMAC secret - must match the handler's `signing.secret`. */
69
69
  readonly secret: string
70
70
  /** Seconds until the URL expires (adds `&exp=`). Omit for a stable, cacheable-forever signed URL. */
71
71
  readonly expiresIn?: number
72
72
  }
73
73
 
74
74
  /**
75
- * Mint a **signed** self-hosted image URL on the server — for cases the (stable) `selfHostedLoader`
75
+ * Mint a **signed** self-hosted image URL on the server - for cases the (stable) `selfHostedLoader`
76
76
  * doesn't cover, chiefly **time-limited** access (`expiresIn`) to private images. Server-only (it holds
77
77
  * the secret). Pair with a passthrough loader, or use the signed string as a plain `src`.
78
78
  *
@@ -100,11 +100,11 @@ export function signImageUrl(
100
100
 
101
101
  export interface ImageProps {
102
102
  readonly src: string
103
- /** Intrinsic width (px) — **required**, reserves layout space (no CLS). */
103
+ /** Intrinsic width (px) - **required**, reserves layout space (no CLS). */
104
104
  readonly width: number
105
- /** Intrinsic height (px) — **required** (no CLS). */
105
+ /** Intrinsic height (px) - **required** (no CLS). */
106
106
  readonly height: number
107
- /** Alt text — **required** for accessibility (use `alt=""` for decorative images). */
107
+ /** Alt text - **required** for accessibility (use `alt=""` for decorative images). */
108
108
  readonly alt: string
109
109
  /** `sizes` attribute for responsive selection (e.g. `"(max-width: 600px) 100vw, 600px"`). */
110
110
  readonly sizes?: string
@@ -147,7 +147,7 @@ export function resolveImage(
147
147
  props.height <= 0
148
148
  ) {
149
149
  throw new Error(
150
- `[nifra/image] <Image> requires positive width + height (got ${props.width}×${props.height}) — they reserve layout space to prevent CLS.`,
150
+ `[nifra/image] <Image> requires positive width + height (got ${props.width}×${props.height}) - they reserve layout space to prevent CLS.`,
151
151
  )
152
152
  }
153
153
  const eager = props.priority === true || props.loading === "eager"
@@ -164,7 +164,7 @@ export function resolveImage(
164
164
  const allSame = entries.every((e) => e.url === entries[0]?.url)
165
165
  const srcSet = allSame ? undefined : entries.map((e) => `${e.url} ${e.w}w`).join(", ")
166
166
 
167
- // `src` is the 1× (intrinsic-width) variant — the fallback for browsers ignoring srcSet.
167
+ // `src` is the 1× (intrinsic-width) variant - the fallback for browsers ignoring srcSet.
168
168
  const base: ResolvedImage = {
169
169
  src: at(props.width),
170
170
  width: props.width,
package/src/server.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  /**
2
- * `@nifrajs/image/server` — nifra's **self-hosted** image resize endpoint. `createImageHandler` returns a
2
+ * `@nifrajs/image/server` - nifra's **self-hosted** image resize endpoint. `createImageHandler` returns a
3
3
  * `(req: Request) => Promise<Response>` you mount at the path your {@link selfHostedLoader} points to
4
4
  * (e.g. `"/_image"`). It validates the query, safely resolves the source (local file under a `root`, or
5
- * a remote URL on an `allowedOrigins` allowlist — both **fail-closed** against SSRF), then decodes →
5
+ * a remote URL on an `allowedOrigins` allowlist - both **fail-closed** against SSRF), then decodes →
6
6
  * resizes (never upscaling) → re-encodes via a pluggable {@link ImageBackend}. The default
7
7
  * {@link bunImageBackend} uses `Bun.Image` (libjpeg-turbo / libspng / libwebp, off-thread).
8
8
  *
9
9
  * This subpath touches the filesystem and a native codec, so it is for **Node/Bun servers**, not the
10
- * edge. On Workers / Vercel-Edge / Deno-Deploy there is no native codec — use the CDN `cloudflareLoader`
10
+ * edge. On Workers / Vercel-Edge / Deno-Deploy there is no native codec - use the CDN `cloudflareLoader`
11
11
  * from `@nifrajs/image` instead. The dependency-free core (`@nifrajs/image`) never imports this module.
12
12
  */
13
13
 
@@ -24,7 +24,7 @@ import {
24
24
  import { verifyImageParams } from "./sign.ts"
25
25
 
26
26
  // The codec seam + every official backend (Bun/sharp/WASM) live in the edge-safe `./backend.ts`
27
- // (`@nifrajs/image/backends`), importable WITHOUT this module's `node:fs` — so a WASM backend can ship to
27
+ // (`@nifrajs/image/backends`), importable WITHOUT this module's `node:fs` - so a WASM backend can ship to
28
28
  // the edge. Re-export them so `@nifrajs/image/server` stays the single import for Node servers.
29
29
  export * from "./backend.ts"
30
30
 
@@ -39,7 +39,7 @@ export interface ImageHandlerOptions {
39
39
  readonly allowedOrigins?: readonly string[]
40
40
  /** Max bytes read from a source before rejecting (413). Default 20 MiB. */
41
41
  readonly maxSourceBytes?: number
42
- /** Max source pixels (w×h) before rejecting (413) — decompression-bomb guard. Default 40 MP. */
42
+ /** Max source pixels (w×h) before rejecting (413) - decompression-bomb guard. Default 40 MP. */
43
43
  readonly maxSourcePixels?: number
44
44
  /** Hard cap on the requested width; larger `?w` is clamped down. Default 3840. */
45
45
  readonly maxWidth?: number
@@ -56,7 +56,7 @@ export interface ImageHandlerOptions {
56
56
  readonly fetch?: typeof fetch
57
57
  /** Require **signed URLs**: every request must carry a valid `&s=` HMAC over `(src, w, q[, exp])` or
58
58
  * it's rejected with `403`. Use the SAME `secret` as your `selfHostedLoader`/`signImageUrl`. This
59
- * locks the endpoint to URLs your app minted — the defense against resize-bombing. */
59
+ * locks the endpoint to URLs your app minted - the defense against resize-bombing. */
60
60
  readonly signing?: { readonly secret: string }
61
61
  }
62
62
 
@@ -114,12 +114,12 @@ function resolveConfig(options: ImageHandlerOptions): ResolvedConfig {
114
114
  }
115
115
 
116
116
  async function handle(req: Request, cfg: ResolvedConfig): Promise<Response> {
117
- // 1. Method — only safe reads. (HEAD shares the GET path; the body is stripped at the end.)
117
+ // 1. Method - only safe reads. (HEAD shares the GET path; the body is stripped at the end.)
118
118
  if (req.method !== "GET" && req.method !== "HEAD") {
119
119
  return errorResponse(405, "method_not_allowed", { Allow: "GET, HEAD" })
120
120
  }
121
121
 
122
- // 2. Validate the query at the trust boundary (strict scalar parsing — no Number() coercion).
122
+ // 2. Validate the query at the trust boundary (strict scalar parsing - no Number() coercion).
123
123
  const url = new URL(req.url)
124
124
  const src = url.searchParams.get("src")
125
125
  if (src === null || src.length === 0 || src.length > MAX_SRC_LEN) {
@@ -138,7 +138,7 @@ async function handle(req: Request, cfg: ResolvedConfig): Promise<Response> {
138
138
  const wantsWebp = (req.headers.get("accept") ?? "").includes("image/webp")
139
139
 
140
140
  // 2b. Signed-URL enforcement (when configured): reject any request we didn't mint. Verifies the
141
- // `&s=` HMAC over the raw (src, w, q[, exp]) — exactly what the loader/`signImageUrl` signed —
141
+ // `&s=` HMAC over the raw (src, w, q[, exp]) - exactly what the loader/`signImageUrl` signed -
142
142
  // and the expiry. Done before the ETag/fetch so unsigned/forged requests cost nothing.
143
143
  if (cfg.signing !== null) {
144
144
  const sig = url.searchParams.get("s")
@@ -204,7 +204,7 @@ async function handle(req: Request, cfg: ResolvedConfig): Promise<Response> {
204
204
  if (err.kind === "too_large") return errorResponse(413, "source_too_large")
205
205
  return errorResponse(415, "unsupported_media_type") // decode | unsupported
206
206
  }
207
- // Unexpected — never leak internals to the client.
207
+ // Unexpected - never leak internals to the client.
208
208
  return errorResponse(500, "internal_error")
209
209
  } finally {
210
210
  cfg.release()
@@ -238,7 +238,7 @@ async function fetchRemote(url: URL, cfg: ResolvedConfig): Promise<SourceResult>
238
238
  let res: Response
239
239
  try {
240
240
  res = await cfg.fetchImpl(url, {
241
- redirect: "error", // a redirect could bounce to a disallowed origin — refuse it.
241
+ redirect: "error", // a redirect could bounce to a disallowed origin - refuse it.
242
242
  signal: AbortSignal.timeout(cfg.fetchTimeoutMs),
243
243
  headers: { Accept: "image/*" },
244
244
  })
@@ -256,7 +256,7 @@ async function fetchRemote(url: URL, cfg: ResolvedConfig): Promise<SourceResult>
256
256
 
257
257
  async function readLocal(src: string, cfg: ResolvedConfig): Promise<SourceResult> {
258
258
  if (cfg.root === null) return { ok: false, status: 403, code: "source_not_allowed" }
259
- // `src` is already percent-decoded by URLSearchParams — do NOT decode again (double-decode bypass).
259
+ // `src` is already percent-decoded by URLSearchParams - do NOT decode again (double-decode bypass).
260
260
  if (src.includes("\0")) return { ok: false, status: 400, code: "invalid_src" }
261
261
  // Strip leading slashes so a site-absolute ("/hero.jpg") or protocol-relative ("//evil") path is
262
262
  // treated as root-relative; `resolve` then collapses any "../" and the containment check rejects it.
@@ -273,7 +273,7 @@ async function readLocal(src: string, cfg: ResolvedConfig): Promise<SourceResult
273
273
  }
274
274
  if (!info.isFile()) return { ok: false, status: 404, code: "source_not_found" }
275
275
  if (info.size > cfg.maxSourceBytes) return { ok: false, status: 413, code: "source_too_large" }
276
- // Defense-in-depth: a symlink inside root could point outside it — re-check the real path.
276
+ // Defense-in-depth: a symlink inside root could point outside it - re-check the real path.
277
277
  try {
278
278
  const real = await realpath(resolved)
279
279
  const realRoot = await realpath(cfg.root)
@@ -287,7 +287,7 @@ async function readLocal(src: string, cfg: ResolvedConfig): Promise<SourceResult
287
287
  }
288
288
 
289
289
  /** Read a response body, aborting (→ null) once `limit` bytes are exceeded. Trusts neither the
290
- * `Content-Length` header (checked as a fast reject) nor an absent one — the running total is the gate. */
290
+ * `Content-Length` header (checked as a fast reject) nor an absent one - the running total is the gate. */
291
291
  async function readBoundedBytes(res: Response, limit: number): Promise<Uint8Array | null> {
292
292
  const declared = res.headers.get("content-length")
293
293
  if (declared !== null && /^\d+$/.test(declared) && Number(declared) > limit) return null
@@ -371,7 +371,7 @@ function errorResponse(
371
371
  }
372
372
 
373
373
  /**
374
- * Async counting semaphore with direct slot hand-off — a released slot is passed straight to the next
374
+ * Async counting semaphore with direct slot hand-off - a released slot is passed straight to the next
375
375
  * waiter (the active count is never transiently decremented), so a concurrent `acquire()` can't slip
376
376
  * past the limit in the microtask gap. Bounds concurrent codec work.
377
377
  */
package/src/sign.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Signed image URLs — a portable, **synchronous** HMAC-SHA256 so the (sync) `selfHostedLoader` can sign
2
+ * Signed image URLs - a portable, **synchronous** HMAC-SHA256 so the (sync) `selfHostedLoader` can sign
3
3
  * URLs inline, on any runtime including the edge (WebCrypto's HMAC is async-only; `node:crypto` isn't on
4
- * Workers). A signed URL lets `createImageHandler` reject any `(src, w, q)` it didn't authorize —
4
+ * Workers). A signed URL lets `createImageHandler` reject any `(src, w, q)` it didn't authorize -
5
5
  * shutting down resize-bombing (width/quality enumeration) and locking the endpoint to your own images.
6
6
  *
7
7
  * Pure JS, dependency-free, KAT-tested against the SHA-256 / RFC 4231 HMAC vectors. The secret never
@@ -124,14 +124,14 @@ export function hmacSha256Hex(key: string, message: string): string {
124
124
  return hex(hmacSha256(encoder.encode(key), encoder.encode(message)))
125
125
  }
126
126
 
127
- /** base64url (RFC 4648 §5, unpadded) of a byte array — URL-safe, compact (43 chars for a 32-byte tag). */
127
+ /** base64url (RFC 4648 §5, unpadded) of a byte array - URL-safe, compact (43 chars for a 32-byte tag). */
128
128
  function base64url(bytes: Uint8Array): string {
129
129
  let binary = ""
130
130
  for (const b of bytes) binary += String.fromCharCode(b)
131
131
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
132
132
  }
133
133
 
134
- /** Constant-time string compare — length-independent of where the first difference is. */
134
+ /** Constant-time string compare - length-independent of where the first difference is. */
135
135
  function safeEqual(a: string, b: string): boolean {
136
136
  if (a.length !== b.length) return false
137
137
  let diff = 0
@@ -154,7 +154,7 @@ export interface ImageSignatureParts {
154
154
  const canonical = (p: ImageSignatureParts): string =>
155
155
  `${p.src}\n${p.w}\n${p.q ?? ""}\n${p.exp ?? ""}`
156
156
 
157
- // 256-bit secret floor — mirrors @nifrajs/core's HMAC guard. This package is dependency-free (ships
157
+ // 256-bit secret floor - mirrors @nifrajs/core's HMAC guard. This package is dependency-free (ships
158
158
  // its own SHA-256), so the constant is inlined rather than imported.
159
159
  const MIN_SECRET_BYTES = 32
160
160