2.0 beta — new releases can include breaking changes. Stable 1.x docs
dimah-s3v2.0.0-beta.0

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 { dimahS3, route, s3Error } from "@dimah-s3/server";
import { awsS3 } from "./aws-s3";

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 s3Error("UNAUTHORIZED"); 
        }, 
      },
    }),
  },
});

Download lifecycle flow

Download flow
  1. route.guardhook
  2. download.resolvehook
  3. download.guardhook
  4. HeadObjectS3
  5. onPresignedhook
  6. Fetch from S3client
  1. resolve: Optionally rewrites the download name, disposition (inline vs attachment), expiresIn, or, in proxy mode, cacheControl. A returned expiresIn replaces the route cap and any client request, then is capped by the instance maxExpiresIn.
  2. guard: Authorizes the request for this key, with the values that will be used. expiresIn is the TTL that will be signed, and undefined in proxy mode.
  3. HeadObject: Reads the stored type, to decide whether inline is allowed.
  4. onPresigned: Receives the URL and the disposition that was signed.

A client may send expiresIn on the download query. The signed TTL is the shorter of that request and download.maxExpiresIn (default 10 minutes) unless resolve replaces it. GET /routes reports the cap as download.maxExpiresIn.

In proxy mode (mode: "proxy"), the URL points to GET /file, the response has no expiresIn, and every request to that URL runs the guards again.


Open in the browser only for known types

A client can ask for disposition: "inline". The server keeps it only for types on the route's allow-list, and signs everything else as attachment. The default list is DEFAULT_INLINE_TYPES from @dimah-s3/core:

  • image/png, image/jpeg, image/gif, image/webp, image/avif, image/bmp
  • application/pdf (presign mode only)
  • audio/*, video/*
  • text/plain

Set inlineTypes to change it, or false to never open inline:

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

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 effective 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.


Type reference

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

DownloadGuardContext

Prop

Type


DownloadOnPresignedContext

Prop

Type


DownloadResolveContext

Prop

Type


DownloadResolveInfo

Prop

Type

Frequently asked questions

On this page