ipx

repository·main·Indexed 25 days ago

https://github.com/unjs/ipx

A high-performance, secure image optimizer powered by sharp and svgo. IPX allows developers to serve transformed images (resized, re-formatted, etc.) on-the-fly via URL modifiers. It supports local filesystem and HTTP(s) storage, provides programmatic integration for H3 and Express, and includes built-in SVG sanitization and optimization.

Tokens
13.7K
Snippets
20
Records
54
Agent score
81%

What's inside ipx

  1. Overview of IPX

    main
    IPX is a high-performance, secure, and easy-to-use image optimizer. It is powered by sharp and svgo. It allows you to serve images from a specific directory or a list of allowed domains, providing on-the-fly transformations (size, format, quality) via URL modifiers.
  2. Secure remote fetching with `blockPrivateIPs`

    main

    When using ipxHttpStorage, setting blockPrivateIPs: true provides a second line of defense against SSRF. It ensures that the host of the requested URL and every redirect hop resolves to a public IP address. It rejects loopback, RFC1918, CGNAT, link-local, and other reserved/private ranges.

    Note: This is off by default to allow fetching from internal cluster origins (like sidecars or local object storage) in development or specific deployments.

    ipxHttpStorage({ domains: ["cdn.example.com"], blockPrivateIPs: true });
  3. How SVG sanitization works in IPX

    main

    To prevent XSS, IPX always sanitizes SVG documents. Sanitization is independent of optimization. IPX removes:

    • <script> elements and event handler attributes (on*).
    • Embedded foreign documents (<foreignObject>, <iframe>, etc.).
    • SMIL animations that use unsafe URIs.
    • URIs with dangerous schemes (e.g., javascript:).
    • <!DOCTYPE> declarations and processing instructions.

    External references like <image href="..."> or <use> are preserved.

    Warning: Sanitization can be disabled with unsafeSkipSanitize: true, but this should only be done if the source is fully trusted, as it allows XSS payloads to pass through unchanged.

    createIPX({
      storage,
      svg: {
        // Serve SVG images unsanitized. Only for fully trusted sources!
        unsafeSkipSanitize: false,
      },
    });
  4. How IPX modifiers work

    main

    IPX uses modifiers in the URL path to transform images. Modifier arguments are separated by underscores (_).

    Validation and Errors

    • 400 IPX_INVALID_MODIFIER_ARG: An argument was provided but is invalid.
    • 400 IPX_MISSING_MODIFIER_ARG: A required argument for a modifier is missing.
    • 400 IPX_INVALID_MODIFIER: The modifier could only be validated by libvips after the pipeline was set up.

    Argument Syntax

    • Colours: Accept hex codes (e.g., f00, ff0000, ff000080) or CSS colour names (e.g., red). Note that the leading # cannot be used in a URL path and should be omitted.
    • Booleans: Accept true/false or the shorthand 1/0.
    • Trailing Arguments: May be omitted to use the default value, unless the modifier requires them.
  5. Understand IPX Image URL structure

    main

    IPX uses a specific URL pattern to apply transformations to images. The structure is:

    /<modifiers>/<id>

    • Modifiers: A comma-separated list of transformation instructions.
    • Arguments: Within a modifier, arguments are separated by an underscore (_).
    • No modifier: Use _ alone to request the original image.
    • ID: The path or identifier of the source image.

    Example URL Patterns

    URLResult
    /_/static/buffalo.pngThe original image.
    /w_200/static/buffalo.pngWidth set to 200, original format (png) kept.
    /f_webp/static/buffalo.pngFormat changed to webp, everything else kept as in the source.
    /f_auto/static/buffalo.pngBest format for the client (avif/webp/jpeg), negotiated from the browser's accept header.
    /s_200x200,fit_contain,f_webp/static/buffalo.pngResized to fit inside 200x200px on a background canvas and converted to webp.
    /<modifiers>/<id>
  6. Configure IPX via environment variables

    main
    Every configuration option can be set globally using IPX_* environment variables. Explicit options passed to the programmatic API will take precedence over environment variables.
  7. Mount IPX as a handler in H3

    main

    To use IPX within an h3 application, use the createIPXFetchHandler function to create a handler from your IPX instance.

    import { H3, serve } from "h3";
    import {
      createIPX,
      ipxFSStorage,
      ipxHttpStorage,
      createIPXFetchHandler,
    } from "ipx";
    
    const ipx = createIPX({
      storage: ipxFSStorage({ dir: "./public" }),
      httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
      alias: { "/picsum": "https://picsum.photos" },
    });
    
    const app = new H3();
    app.mount("/ipx", createIPXFetchHandler(ipx));
    
    serve(app);
  8. Configure SVG processing and optimization

    main

    IPX processes SVG images by sanitizing them and optionally optimizing them with svgo.

    Optimization: By default, SVGO's preset-default is applied. This can break SVGs that rely on specific IDs for external references (like sprite sheets) or internal CSS selectors. To sanitize without optimizing, use optimize: false. To keep optimization but prevent ID renaming or element removal, configure the plugins manually.

    // Sanitize without optimizing
    createIPX({
      storage,
      svg: {
        optimize: false,
      },
    });
    
    // Optimize with specific plugin overrides to preserve IDs/elements
    createIPX({
      storage,
      svg: {
        optimize: {
          plugins: [
            {
              name: "preset-default",
              params: {
                overrides: { cleanupIds: false, removeHiddenElems: false },
              },
            },
          ],
        },
      },
    });
  9. Customize the URL parsing style with `parseURL`

    main

    You can define a custom URL structure by providing a parseURL function to the createIPXFetchHandler options. This function is responsible for extracting the resource id and the modifiers object from the incoming request URL.

    Key details for implementing a custom parser:

    • Input: The function receives the raw, percent-encoded request URL.
    • Async Support: The parser can be an async function.
    • Error Handling: You can throw an HTTPError (re-exported from ipx) to reject a request with a specific status code.
    • Escaping: Returned values for id and modifiers are automatically escaped by IPX; you do not need to manually escape them.
    • Security: Custom parsers do not bypass security. The resulting id is still subject to the constraints of your storage layer (e.g., ipxFSStorage directory boundaries or ipxHttpStorage domain allowlists).

    This is useful for styles like /<id>@@<modifiers>.<format>, which is often preferred for static hosting and prerendering.

    import { createIPXFetchHandler, parseIPXURL } from "ipx";
    
    const handler = createIPXFetchHandler(ipx, {
      parseURL(url) {
        const path = decodeURIComponent(new URL(url).pathname.slice(1));
    
        const match = path.match(/^(.+)@@(.+)\.([^.]+)$/);
        if (!match) {
          // Not our style, fall back to the default `/<modifiers>/<id>`
          return parseIPXURL(url);
        }
    
        const [, id = "", modifiersString = "", format = ""] = match;
        const modifiers = Object.fromEntries(
          modifiersString.split(",").map((m) => {
            const [key = "", ...values] = m.split("_");
            return [key, values.join("_")];
          }),
        );
    
        return { id, modifiers: { ...modifiers, format } };
      },
    });
    
    // http://localhost:3000/static/buffalo.png@@s_200x200.webp
    // http://localhost:3000/static/buffalo.png@@grayscale,w_200.webp
  10. Mount IPX as a handler in Express

    main

    To use IPX within an express application, use the createIPXNodeHandler function. Note that you may need to cast the result to a RequestHandler for TypeScript compatibility.

    import Express from "express";
    import {
      createIPX,
      ipxFSStorage,
      ipxHttpStorage,
      createIPXNodeHandler,
    } from "ipx";
    import type { RequestHandler } from "express";
    
    const ipx = createIPX({
      storage: ipxFSStorage({ dir: "./public" }),
      httpStorage: ipxHttpStorage({ domains: ["picsum.photos"] }),
      alias: { "/picsum": "https://picsum.photos" },
    });
    
    const app = Express();
    app.use("/ipx", createIPXNodeHandler(ipx) as RequestHandler);
    
    app.listen(3000);