Guards and Locals
The instance guard, guard order, and passing typed locals to later hooks.
Define guard on dimahS3() to run it before every operation, before the
route is looked up. Plugin endpoints run it too, with operation: "plugin".
Use it for system-wide checks like user authentication, IP filtering, or tenant context.
A signed-in session here does not prove the caller owns a particular object
key — that belongs on download.guard / delete.guard or db(). 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!,
guard: async ({ request }) => {
const session = await getSession(request);
if (!session) throw s3Error("UNAUTHORIZED");
},
routes: {
avatar: route({
upload: {
fileTypes: ["image/*"],
maxFileSize: 2 * 1024 * 1024,
},
}),
},
});What a guard returns
A guard allows a call by returning nothing, and rejects it by throwing:
| Guard does | Result |
|---|---|
| Returns nothing | Allowed |
Returns { locals } | Allowed; locals is passed to later hooks |
Throws s3Error(code) | Rejected with that status and code |
Throws a plain Error | 500 INTERNAL_ERROR, logged |
Returns anything else (true, false, …) | Config error: 500, logged, never allowed |
guard: () => isAdmin() does not work: a boolean is never read as an
answer. Throw to reject.
Guard order
Every operation runs its guards in this order:
- Instance guardhook
- route.guardhook
- Feature guardupload.guard, download.guard, …
- Operationserver
At each level, plugin guards run first, in plugin order, and yours runs last.
operation in the context names the call ("upload", "confirm",
"download", "file", "multipart.signPart", "plugin", …), so one guard
can branch on it.
Locals
Return { locals } from a guard to hand values to every later guard and hook
in the same call. Resolve the session once in the instance guard, and read it
everywhere else:
declare module "@dimah-s3/server" {
interface DimahS3Locals {
userId?: string;
role?: "member" | "admin";
}
}
export const s3 = dimahS3({
guard: async ({ request }) => {
const session = await getSession(request);
if (!session) throw s3Error("UNAUTHORIZED");
return { locals: { userId: session.userId, role: session.role } };
},
routes: {
reports: route({
download: {
guard: ({ locals }) => {
if (locals.role !== "admin") throw s3Error("FORBIDDEN");
},
},
}),
},
});- Each call gets its own null-prototype
localsobject. Each{ locals }result is merged into it. - An HTTP request can never set
locals. Only guards, and in-process calls throughs3.apiands3.put, can. - In-process
options.localsseeds the object before any guard runs:
await s3.api.download(
{ route: "reports", key },
{ locals: { userId: "system", role: "admin" } },
);ticketSubject sees the locals from the instance and route guards only. See
Upload Tickets.
Type reference
import type {
DimahS3Locals,
GuardContext,
GuardResult,
RouteGuardContext,
} from "@dimah-s3/server";GuardContext
Prop
Type
RouteGuardContext
Prop
Type
Frequently asked questions
Use route.guard when only some routes need the policy. For example, an app
with public media uploads and private invoices can leave the global guard
unset and protect specific routes:
export const s3 = dimahS3({
routes: {
publicMedia: route({
upload: true,
}),
avatar: route({
guard: requireUserSession,
upload: true,
}),
},
});The route guard runs before every operation on that route, confirm and multipart calls included.
Configure headers in createS3Client. Per-call headers on a method
override them:
export const s3Client = createS3Client({
headers: async () => ({
Authorization: `Bearer ${await getToken()}`,
}),
});