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.
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
- route.guardhook
- download.resolvehook
- download.guardhook
- HeadObjectS3
- onPresignedhook
- Fetch from S3client
resolve: Optionally rewrites the downloadname,disposition(inlinevsattachment),expiresIn, or, in proxy mode,cacheControl. A returnedexpiresInreplaces the route cap and any client request, then is capped by the instancemaxExpiresIn.guard: Authorizes the request for thiskey, with the values that will be used.expiresInis the TTL that will be signed, andundefinedin proxy mode.- HeadObject: Reads the stored type, to decide whether
inlineis allowed. 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/bmpapplication/pdf(presign mode only)audio/*,video/*text/plain
Set inlineTypes to change it, or false to never open inline:
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 asupload.fileTypes): the requestedinlineis 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-streamis typed from its file name. When that type is allowed, it is signed asResponseContentTypeso 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
Yes, up to the route cap. download.maxExpiresIn is the maximum. A shorter
expiresIn on the query is signed as requested. Return expiresIn from
resolve when the server should choose a different TTL, including one
longer than the route cap. That value is still capped by the instance
maxExpiresIn (7 days at most).
files: route({
download: {
maxExpiresIn: 60 * 60 * 24,
},
}),Use download.resolve to set the downloaded file name per request:
avatar: route({
download: {
resolve: async ({ key }) => {
const user = await getUserByKey(key);
return {
name: `${user.username}-avatar.png`,
disposition: "attachment",
};
},
},
}),Set mode: "proxy". The browser receives a URL on your API
(/api/s3/file?route=…&key=…), and your server streams the object instead of
returning a direct bucket URL. See File Proxy.
avatar: route({
download: {
mode: "proxy",
},
}),