2.0 beta is ready. Install with @beta.
dimah-s3v1.7.2

Download Hooks

Authorize and sign GET download URLs or proxy streams.

Enable download operations with download: true or a DownloadConfig object on a named route.

The key must sit under this route's keyPrefix. That is not ownership — a session-only guard still lets any signed-in user download any namespaced key they know. See Security.

lib/s3.ts
import { S3Client } from "@aws-sdk/client-s3";
import { dimahS3, errors, route } from "@dimah-s3/server";

export const awsS3 = new S3Client({/* env */});

export const s3 = dimahS3({
  client: awsS3,
  bucket: process.env.S3_BUCKET!,
  routes: {
    avatar: route({
      download: {
        disposition: "inline",
        guard: async ({ key, request }) => {
          const session = await getSession(request); 
          if (!session) throw errors.unauthorized(); 
        }, 
      },
    }),
  },
});

Download lifecycle flow

Download flow
  1. download.resolvehook
  2. download.guardhook
  3. onPresignedhook
  4. Fetch from S3S3
  1. resolve: Optionally rewrites download fileName, disposition (inline vs attachment), or expiresIn. A returned expiresIn replaces the route cap and any client request, then is clamped to maxExpiresIn.
  2. guard: Validates request permissions for the specified key. expiresIn here is the TTL that will be signed (0 when mode is "proxy").
  3. onPresigned: Receives the signed URL and expiration details.

A client may send expiresIn on the download query. The signed TTL is the shorter of that request and download.expiresIn (default 10 minutes) unless resolve replaces it. GET /routes includes the cap as download.expiresIn. Proxy downloads ignore it and return expiresIn: 0.


Open in the browser only for known types

A client can ask for disposition: "inline". Without a limit, any stored object — HTML or SVG included — would then render in a browser tab. Set inlineTypes to the types this route may open inline:

lib/s3.ts
files: route({
  download: {
    inlineTypes: ["image/*", "application/pdf", "video/mp4", "text/plain"], 
  },
}),

After the guards, the server reads the object's type from HeadObject:

  • On the list (image/* matches the same way as upload.fileTypes): the requested inline is kept.
  • Anything else is signed as attachment, whatever the client asked for.
  • A wildcard never matches types that run as a page: HTML, any XML (including image/svg+xml), and JavaScript. List one exactly to allow it.
  • An object stored as application/octet-stream is typed from its file name. When that type is allowed, it is signed as ResponseContentType so the browser renders it.

GET /routes returns the list as download.inlineTypes. A client can pass it to resolveInlineDisposition from @dimah-s3/core to choose Preview or Download with the server's rule. The same helper works inside resolve. Proxy responses (GET /file) also send X-Content-Type-Options: nosniff.


Type reference

import type {
  DownloadGuardContext,
  DownloadOnPresignedContext,
  DownloadResolveInfo,
} from "@dimah-s3/server";

DownloadGuardContext

Prop

Type


DownloadOnPresignedContext

Prop

Type


DownloadResolveInfo

Prop

Type

Frequently asked questions

On this page