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

@westayltd/authz

v0.3.3

Published

Business-agnostic NestJS authorization library: RequirePermission decorator + AuthzGuard backed by Auth Service /rbac/me.

Readme

@westayltd/authz

Business-agnostic NestJS authorization library. It enforces permissions on routes by calling Auth Service's frozen GET /rbac/apps/:appKey/me contract, caching grants as two keys:

  • rbac:me:{app}:{principalType}:{principalId} → { roleKey, status }
  • rbac:perms:{app}:{roleKey} → permissions[]

and failing closed if Auth Service is unreachable.

authServiceBaseUrl must already include the Auth API prefix (e.g. https://host/auth-service/api/v1). The client appends rbac/me only.

This package contains zero business permission keys, role names, or app names. Every host application owns its own permissions.ts / rbac.manifest.ts and imports that manifest into Auth Service separately — this library never sees business permission strings except as opaque values passed to @RequirePermission(...).

Install

Until published, consume via a local path or git dependency:

{
  "dependencies": {
    "@westayltd/authz": "file:../westay-authz"
  }
}

Wiring

import { Module } from '@nestjs/common';
import { AuthzModule } from '@westayltd/authz';
import { RedisAuthzCacheAdapter } from '@westayltd/authz';

@Module({
  imports: [
    AuthzModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        appKey: 'catalogue',
        authServiceBaseUrl: config.get('AUTH_SERVICE_URL'),
        cacheTtlSeconds: 300,
        cacheAdapter: new RedisAuthzCacheAdapter(redisClient),
      }),
    }),
  ],
})
export class AppModule {}

AuthzModule exports AuthzGuard, PermissionResolver, and AuthzClient for use in any module that imports it.

Protecting an endpoint

AuthzGuard must run after the host app's own JWT guard — it expects request.user (populated by the JWT guard) and the original Authorization header to forward to Auth Service.

import { Controller, Post, UseGuards } from '@nestjs/common';
import { StaffJwtGuard } from '../auth/staff-jwt.guard';
import { AuthzGuard, RequirePermission } from '@westayltd/authz';
import { CataloguePermissions } from '../authz/permissions';

@Controller('properties')
@UseGuards(StaffJwtGuard, AuthzGuard)
export class PropertiesController {
  @Post()
  @RequirePermission(CataloguePermissions.PROPERTY_CREATE)
  create() {
    /* ... */
  }
}

A route with no @RequirePermission stays JWT-only — AuthzGuard allows it without contacting Auth Service.

When a required permission is granted, the guard attaches the resolved grant on the request so the handler/service can apply own vs all (or similar) checks without calling /me again:

import { RequestAuthz } from '@westayltd/authz';

const authz: RequestAuthz | undefined = request.authz;
const hasAllScope = authz?.has('example.resource.all') ?? false;

request.authz is only set on a granted @RequirePermission check. JWT-only routes and deny outcomes do not populate it.

Behavior

  • Both cache keys hit → authorize locally, no HTTP call.
  • Either key miss → GET /rbac/apps/:appKey/me, cache assignment + role perms on success only.
  • Cached status: inactive → 403 without calling Auth Service.
  • Deny outcomes (401/403/503) are never cached as grants, so grants/revocations from Auth Service take effect immediately after Auth invalidates Redis.
  • Auth Service unreachable after one retry → 503 Service Unavailable (fail closed — never silently allow).
  • Missing permission → 403 Forbidden.
  • Missing/invalid principal or session → 401 Unauthorized.

The consumer Redis instance must be the same host and db Authzilla writes to (default db 0). A mismatched db means invalidation never reaches this app.

Cache adapters

  • NoopAuthzCacheAdapter (default if none supplied): always a cache miss — useful for local dev/tests without Redis. The module logs a warning at boot if you don't pass a cacheAdapter, since every authorized request will hit Auth Service.
  • RedisAuthzCacheAdapter: pass an existing ioredis client instance you already manage (connection lifecycle, retries, TLS, etc. stay the host app's responsibility — this library never opens its own Redis connection):
import Redis from 'ioredis';
import { RedisAuthzCacheAdapter } from '@westayltd/authz';

const redisClient = new Redis(process.env.REDIS_URL);

AuthzModule.forRootAsync({
  useFactory: () => ({
    appKey: 'catalogue',
    authServiceBaseUrl: process.env.AUTH_SERVICE_URL,
    cacheTtlSeconds: 300,
    cacheAdapter: new RedisAuthzCacheAdapter(redisClient),
  }),
});

If Redis itself errors (connection drop, timeout, etc.), RedisAuthzCacheAdapter logs a warning and returns as if it were a cache miss/no-op — it never throws. This means a Redis outage degrades to "every check calls Auth Service" rather than breaking authorization. If Auth Service is also unreachable at that point, the guard still fails closed (see Outage behavior below) — a cache-layer outage never turns into an "allow".

Implement AuthzCacheAdapter yourself for any other backing store.

Outage behavior (fail closed)

V1 only supports failMode: 'closed' — AuthzModule.forRoot/forRootAsync throw at boot if any other value is configured. Concretely:

| Situation | Outcome | | --- | --- | | Both cache keys hit | Allow/deny decided locally, no network call. | | Cache miss, Auth Service reachable | Calls /rbac/apps/:appKey/me, both keys cached on success only. | | Cache miss, Auth Service down/unreachable (after 1 retry) | 503 Service Unavailable — deny, never allow. | | Redis down, Auth Service reachable | Cache adapter no-ops; falls through to calling Auth Service on every request (slower, but correct). | | Redis down and Auth Service down | 503 — deny. |

Configuration options (AuthzModuleOptions)

| Option | Required | Description | | --- | --- | --- | | appKey | yes | This app's key as registered in Auth Service RBAC. | | authServiceBaseUrl | yes | Auth API prefix, e.g. https://host/auth-service/api/v1. | | cacheTtlSeconds | yes | TTL for cached grants. | | cacheAdapter | no | Defaults to NoopAuthzCacheAdapter. | | failMode | no | V1 only supports 'closed'. | | principalIdPath | no | Dot path on request for principal id. Default user.id. | | principalTypePath | no | Dot path on request for principal type. Default user.type, falls back to 'staff'. | | requestTimeoutMs | no | Abort timeout per /rbac/apps/:appKey/me call. Default 3000. |

Non-goals

  • No business permission constants, roles, or manifests (owned by each app).
  • No policy engine / ABAC.
  • No event bus based invalidation (Auth Service invalidates cache directly via the service layer).