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

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.

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!,
  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 doesResult
Returns nothingAllowed
Returns { locals }Allowed; locals is passed to later hooks
Throws s3Error(code)Rejected with that status and code
Throws a plain Error500 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:

Guard order
  1. Instance guardhook
  2. route.guardhook
  3. Feature guardupload.guard, download.guard, …
  4. 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:

lib/s3.ts
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 locals object. Each { locals } result is merged into it.
  • An HTTP request can never set locals. Only guards, and in-process calls through s3.api and s3.put, can.
  • In-process options.locals seeds 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

On this page