npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@neoma/garmr

v0.7.0

Published

Passwordless authentication and permission-based authorization for NestJS. Magic links, cookie/bearer sessions, and wildcard permissions.

Readme

@neoma/garmr

Passwordless authentication for NestJS applications. Garmr provides magic link authentication, JWT session management, cookie-based sessions, and route protection out of the box.

Why Passwordless?

Password authentication requires secure hashing, strength validation, reset flows, and breach checking. Magic links eliminate all of this complexity. The email IS the verification - simpler for developers, fewer security footguns.

Features

  • Magic link authentication (send & verify)
  • JWT session tokens with HS256 algorithm enforcement and audience validation
  • Cookie-based sessions (httpOnly, secure, sameSite) with configurable options
  • Dual transport: Bearer token and cookie authentication middlewares
  • Route protection with guards and decorators
  • Permission-based authorization with wildcard support (@RequiresPermission, @RequiresAnyPermission)
  • Email normalization (case-insensitive)
  • Event emission for registration and authentication

Installation

npm install @neoma/garmr

Peer Dependencies

npm install @nestjs/common @nestjs/core @nestjs/platform-express @nestjs/event-emitter @nestjs/typeorm rxjs reflect-metadata class-validator typeorm

Getting Started

1. Create your User entity

Your user entity must implement the Authenticatable interface:

import { Authenticatable } from "@neoma/garmr"
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"

@Entity()
export class User implements Authenticatable {
  @PrimaryGeneratedColumn("uuid")
  public id: string

  @Column({ unique: true })
  public email: string

  @Column("simple-array", { default: "" })
  public permissions: string[]
}

2. Configure GarmrModule

Import and configure GarmrModule in your application module:

import { GarmrModule } from "@neoma/garmr"
import { Module } from "@nestjs/common"
import { TypeOrmModule } from "@nestjs/typeorm"

import { User } from "./user.entity"

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: "postgres",
      // ... your database config
      entities: [User],
    }),
    GarmrModule.forRoot({
      secret: process.env.JWT_SECRET,
      expiresIn: "1h",
      entity: User,
      mailer: {
        host: process.env.SMTP_HOST,
        port: parseInt(process.env.SMTP_PORT),
        from: "[email protected]",
        welcome: {
          subject: "Welcome to YourApp",
          html: '<a href="https://yourapp.com/auth/verify?token={{token}}">Sign up</a>',
        },
        welcomeBack: {
          subject: "Sign in to YourApp",
          html: '<a href="https://yourapp.com/auth/verify?token={{token}}">Sign in</a>',
        },
        auth: {
          user: process.env.SMTP_USER,
          pass: process.env.SMTP_PASS,
        },
      },
      cookie: {
        name: "garmr.sid",  // default
        secure: true,       // default
        sameSite: "lax",    // default
        path: "/",          // default
        // domain: ".yourapp.com",  // optional
      },
    }),
  ],
})
export class AppModule {}

3. Enable validation

Garmr exports EmailDto with class-validator decorators. For validation to work, enable ValidationPipe in your application.

See the NestJS Validation documentation for setup instructions.

4. Create authentication endpoints

Use the provided services to build your authentication endpoints:

import { EmailDto, MagicLinkService, SessionService } from "@neoma/garmr"
import {
  Body,
  Controller,
  Get,
  HttpCode,
  HttpStatus,
  Post,
  Query,
  Res,
} from "@nestjs/common"
import { Response } from "express"

import { User } from "./user.entity"

@Controller("auth")
export class AuthController {
  public constructor(
    private readonly magicLinkService: MagicLinkService,
    private readonly sessionService: SessionService,
  ) {}

  @Post("magic-link")
  @HttpCode(HttpStatus.ACCEPTED)
  public async sendMagicLink(@Body() dto: EmailDto): Promise<void> {
    await this.magicLinkService.send(dto.email)
  }

  @Get("verify")
  public async verify(
    @Query("token") token: string,
    @Res({ passthrough: true }) res: Response,
  ): Promise<{ token: string; user: User; isNewUser: boolean }> {
    const { entity, isNewUser } = await this.magicLinkService.verify<User>(token)
    const { token: sessionToken } = this.sessionService.create(res, entity)
    return { token: sessionToken, user: entity, isNewUser }
  }
}

SessionService.create() issues a session JWT and sets it as an httpOnly cookie on the response. The cookie's Max-Age is automatically aligned with the JWT's expiry.

5. Protect routes

Use the Authenticated guard and Principal decorator to protect routes:

import { Authenticated, Principal } from "@neoma/garmr"
import { Controller, Get, UseGuards } from "@nestjs/common"

import { User } from "./user.entity"

@Controller("me")
@UseGuards(Authenticated)
export class ProfileController {
  @Get()
  public get(@Principal() user: User): { id: string; email: string } {
    return {
      id: user.id,
      email: user.email,
    }
  }
}

GarmrModule automatically applies BearerAuthenticationMiddleware and CookieAuthenticationMiddleware to all routes. They extract the JWT from the Authorization: Bearer <token> header or the garmr.sid cookie respectively, and attach the authenticated user to req.principal. Bearer takes priority when both are present.

Magic Link Flow

  1. User submits email -> POST /auth/magic-link
  2. Server generates JWT with aud: "magic-link" and emails verification link
  3. User clicks link -> GET /auth/verify?token=...
  4. Server validates token, creates user (if new) or finds existing user
  5. Server issues session JWT with aud: "session" and sets httpOnly cookie
  6. Subsequent requests authenticate via cookie or Authorization: Bearer <token> header

Example Requests

Request Magic Link

curl -X POST http://localhost:3000/auth/magic-link \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]"}'

Success (202 Accepted): Empty response, email sent.

Validation error (400 Bad Request):

{
  "statusCode": 400,
  "message": ["Please enter a valid email address."],
  "error": "Bad Request"
}

Verify Magic Link

curl "http://localhost:3000/auth/verify?token=eyJhbGciOiJIUzI1NiIs..."

Success (200 OK):

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "[email protected]"
  },
  "isNewUser": true
}

The response also includes a Set-Cookie header with the session token.

Invalid token (401 Unauthorized):

{
  "statusCode": 401,
  "message": "Invalid magic link token: invalid audience",
  "reason": "invalid audience",
  "error": "Unauthorized"
}

Accessing Protected Routes

Via Bearer token:

curl http://localhost:3000/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Via cookie (set automatically by the verify endpoint):

curl http://localhost:3000/me \
  -b "garmr.sid=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Success (200 OK):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "[email protected]"
}

Not authenticated (401 Unauthorized):

{
  "statusCode": 401,
  "message": "Unable to authenticate a principal. Please check the documentation for accepted authentication methods",
  "error": "Unauthorized"
}

API Reference

GarmrModule

GarmrModule.forRoot(options)

Configures the authentication module. The module is global — import it once in your root module.

| Option | Type | Description | |--------|------|-------------| | secret | string | JWT signing secret | | expiresIn | string \| number | Session token expiration (e.g., "1h", "7d") | | entity | Type<Authenticatable> | Your user entity class | | mailer | MailerOptions | Email configuration (see below) | | cookie | CookieOptions | Session cookie configuration (optional) |

MailerOptions

| Option | Type | Description | |--------|------|-------------| | host | string | SMTP host | | port | number | SMTP port | | from | string | Sender email address | | welcome | MailerTemplate | Template sent to new users (registration) | | welcomeBack | MailerTemplate | Template sent to existing users (login) | | auth.user | string | SMTP username | | auth.pass | string | SMTP password |

MailerTemplate

| Option | Type | Description | |--------|------|-------------| | subject | string | Email subject line | | html | string | Email HTML body (use {{token}} placeholder) |

CookieOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | name | string | "garmr.sid" | Cookie name | | domain | string | — | Cookie domain | | path | string | "/" | Cookie path | | secure | boolean | true | Only send over HTTPS | | sameSite | "strict" \| "lax" \| "none" | "lax" | SameSite attribute |

Services

MagicLinkService

  • send(email: string): Promise<void> - Sends a magic link email (uses welcome template for new users, welcomeBack for existing)
  • verify<T>(token: string): Promise<{ entity: T; isNewUser: boolean }> - Validates token and returns/creates user

SessionService

  • create(res: Response, entity: Authenticatable): { token: string; payload: JwtPayload } - Issues a session JWT and sets it as an httpOnly cookie
  • clear(res: Response): void - Clears the session cookie

AuthenticationService

  • authenticate<T>(token: string): Promise<T> - Validates a raw session JWT and returns the user

TokenService

  • issue(payload, options?): { token: string; payload: JwtPayload } - Issues a JWT (HS256)
  • verify(token: string): JwtPayload - Verifies a token (HS256 only)

Middlewares

BearerAuthenticationMiddleware

Extracts JWT from the Authorization: Bearer <token> header. Throws InvalidCredentialsException for malformed headers. Logs and continues for authentication failures.

CookieAuthenticationMiddleware

Extracts JWT from the configured cookie (default garmr.sid). Logs and continues for authentication failures.

Both middlewares are automatically applied by GarmrModule. Bearer runs first; if it sets req.principal, the cookie middleware skips.

PermissionService

  • hasPermission(principal, permission): boolean - Check if a principal has a specific permission
  • hasAllPermissions(principal, permissions): boolean - Check if a principal has ALL permissions (AND logic)
  • hasAnyPermission(principal, permissions): boolean - Check if a principal has at least one permission (OR logic)
  • requirePermission(principal, permission): void - Throws PermissionDeniedException if missing
  • requireAllPermissions(principal, permissions): void - Throws if any are missing (AND)
  • requireAnyPermission(principal, permissions): void - Throws if all are missing (OR)

Valid permission formats:

  • * — matches all permissions
  • name — single-segment, exact match only (e.g., admin)
  • action:resource — exact match (e.g., read:users)
  • *:resource — matches any action on that resource (e.g., *:articles matches read:articles)
  • action:* — matches an action on any resource (e.g., read:* matches read:users)

Invalid formats (e.g., read:users:admin, empty strings) throw at decoration time or at runtime.

Constants

  • MAGIC_LINK_AUDIENCE - Value: "magic-link" - Used for magic link tokens
  • SESSION_AUDIENCE - Value: "session" - Used for session tokens

Guards

Authenticated

A guard that ensures req.principal exists. Throws UnauthorizedException if not authenticated.

@UseGuards(Authenticated)
@Controller("protected")
export class ProtectedController {}

For server-rendered apps, pass a redirect URL. The guard throws UnauthorizedRedirectException — still a 401, but carrying redirect metadata via getRedirect(). An exception filter may use this to issue an HTTP redirect for browser-based requests instead of returning 401 JSON.

@UseGuards(new Authenticated("/auth/magic-link"))
@Controller("dashboard")
export class DashboardController {}

Decorators

Principal

Extracts the authenticated user from the request.

@Get()
public getProfile(@Principal() user: User): User {
  return user
}

RequiresPermission

Enforces that the authenticated user has all of the specified permissions (AND logic). Automatically applies authentication and the permission guard.

// Single permission
@Get("articles")
@RequiresPermission("read:articles")
public getArticles() {}

// Multiple permissions (AND — user must have both)
@Get("articles/edit")
@RequiresPermission("read:articles", "write:articles")
public editArticles() {}

// Class-level — applies to all methods
@Controller("admin")
@RequiresPermission("read:admin")
export class AdminController {}

RequiresAnyPermission

Enforces that the authenticated user has at least one of the specified permissions (OR logic).

// User must have admin OR delete:articles
@Get("articles/delete")
@RequiresAnyPermission("admin", "delete:articles")
public deleteArticles() {}

Both decorators can be combined on a single method:

// User must have read:reports AND (admin OR write:reports)
@Get("reports")
@RequiresPermission("read:reports")
@RequiresAnyPermission("admin", "write:reports")
public getReports() {}

DTOs

EmailDto

  • email - Required, must be valid email format

Exceptions

| Exception | Status | When | |-----------|--------|------| | InvalidMagicLinkTokenException | 401 | Magic link token invalid or wrong audience | | TokenFailedVerificationException | 401 | JWT verification failed (expired, invalid signature) | | IncorrectCredentialsException | 401 | User not found for valid token | | InvalidCredentialsException | 401 | Token invalid, wrong audience, or malformed header | | UnauthorizedRedirectException | 401 | Unauthenticated request on a route with a redirect URL. Carries redirect metadata via getRedirect() for filters to handle | | PermissionDeniedException | 403 | User lacks required permission(s) |

Events

GarmrRegisteredEvent

Emitted when a new user is created via magic link verification.

import { GarmrRegisteredEvent } from "@neoma/garmr"
import { OnEvent } from "@nestjs/event-emitter"

@Injectable()
export class NotificationService {
  @OnEvent(GarmrRegisteredEvent.EVENT_NAME)
  public async onRegistered(event: GarmrRegisteredEvent): Promise<void> {
    // Send welcome email, etc.
  }
}

GarmrAuthenticatedEvent

Emitted when an existing user verifies a magic link or authenticates via session token.

Interfaces

Authenticatable

interface Authenticatable {
  id: any
  email: string
  permissions?: string[]
}

Implement this on any entity you want to authenticate. The permissions field is optional — only needed if you use the permission decorators.

Security

  • JWTs are signed and verified with HS256 only — other algorithms are rejected
  • Magic link tokens use aud: "magic-link", session tokens use aud: "session" — cross-use is prevented
  • Session cookies are httpOnly (not accessible to JavaScript), secure (HTTPS only), and SameSite=Lax by default
  • Cookie Max-Age is automatically aligned with JWT expiry
  • Error responses use a generic message — internal details are logged server-side only
  • Email lookups are case-insensitive (normalized to lowercase)
  • Magic links expire after 15 minutes

License

MIT