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

@generazioneai/authz

v0.3.8

Published

Agnostic runtime authorization for NestJS + Prisma: CASL scoping, signed inter-service JWTs, and snapshot-based permission propagation over NATS.

Readme

@generazioneai/authz

Agnostic, fail-closed runtime authorization for NestJS + Prisma.

A small, composable engine for multi-tenant authorization across a microservice mesh:

  • Query-level scoping — a Prisma Client extension ANDs the caller's CASL ability into every where (incl. nested include/select), so a row a user can't see is impossible to read/update/delete/connect — even from a bug.
  • Snapshot-based permissions — one service builds a user's permission snapshot (roles → CASL rules) once, caches it in Redis under an opaque id; every other service rehydrates it by id. No N+1 permission lookups.
  • Signed inter-service calls — short-lived EdDSA JWTs authenticate every NATS message (replay-protected, body-hash-bound, cmd-bound).
  • Default-deny route guards@Public() / @Authenticated() / @Authorize(action, subject).

Agnostic: the engine knows nothing about your entities, tables, identity model, or service names. Your domain lives in resource manifests you write in each service; the SDK only provides the mechanism.

npm i @generazioneai/authz @casl/ability @casl/prisma
# per-subpath extras: ioredis (snapshot/nats), jose (dep), @nestjs/* (nest), @prisma/client (peer)

Pre-1.0: minor versions (0.x) may break.


Modes

Two independent env switches, both default off (adding the SDK to a running service changes nothing):

| var | read by | off | shadow | enforce | |---|---|---|---|---| | AUTHZ_ENFORCE_MODE | the Prisma extension | passthrough | measure + log | scope queries, fail-closed | | AUTHZ_INTERNAL_AUTH_MODE | the inter-service interceptor | strip token | verify + log | verify + reject |

Enable them together. The Prisma extension reads the ability from AsyncLocalStorage, which is populated by the inter-service interceptor. AUTHZ_ENFORCE_MODE=enforce with AUTHZ_INTERNAL_AUTH_MODE=off means no context → every query throws UNSCOPED_QUERY. (Reference services fail-fast at boot on this combo.)


Concepts

| Piece | What it is | |---|---| | Resource manifest | defineResource({ subject, prismaModel, tenancy, scopes, actions, autoquery }) — how a model is scoped. Lives in your service. | | AuthzContext | Per-request payload in AsyncLocalStorage: { userId, tenantId?, ability, …claims }. Open shape — attach any domain claims your $ctx paths reference. | | Scope template | A Prisma where with { $ctx: 'path' } placeholders, substituted from the context (e.g. { ownerId: { $ctx: 'userId' } }). | | Snapshot | Serialized CASL rules + raw grants + claims, cached in Redis under an opaque snapId. |

The context is yours

The SDK reads only userId, tenantId, ability, and a few engine internals. Everything else is your vocabulary, attached as claims and resolved by $ctx paths in your manifests:

import type { AuthzContext } from '@generazioneai/authz';

interface AppContext extends AuthzContext {   // purely for your own type-safety
  orgId?: string;
  teamIds?: string[];
}
// a manifest scope can then reference { $ctx: 'orgId' } or { $ctx: 'teamIds' }

1. Declare resources

// post.resource.ts  (in your service)
import { defineResource } from '@generazioneai/authz';

export const PostResource = defineResource({
  subject: 'Post',
  prismaModel: 'post',                         // must match the Prisma model (camelCase)
  service: 'blog-api',                         // free-form metadata
  tenancy: { kind: 'single', field: 'orgId' }, // stamped on create
  actions: ['create', 'read', 'update', 'delete'],
  scopes: {
    TENANT: { orgId: { $ctx: 'orgId' } },      // members of an org
    OWN:    { authorId: { $ctx: 'userId' } },  // the author
    // GLOBAL needs no template (unrestricted within the grant)
  },
  autoquery: { filterable: {}, sortable: [], includable: [], pagination: { default: 50, max: 200 } },
});

// registry.ts
import { ResourceRegistry } from '@generazioneai/authz';
export const registry = new ResourceRegistry();
[PostResource /*, ... */].forEach((m) => registry.register(m));

In enforce, a query on a model with no manifest throws UNMANAGED_MODEL — fail-closed. Genuinely-unmanaged tables (e.g. OIDC adapter rows) go in allowUnmanaged (below).

2. Enforce on every query (Prisma extension)

import { Prisma, PrismaClient } from '@prisma/client';
import { createAuthzPrismaExtension } from '@generazioneai/authz/enforce';

let client: any;
client = new PrismaClient().$extends(
  createAuthzPrismaExtension(registry, {
    mode: process.env.AUTHZ_ENFORCE_MODE,            // default 'off'
    dmmf: Prisma.dmmf.datamodel.models,              // required to scope nested include/select
    allowUnmanaged: ['OidcEntity'],                  // models intentionally unscoped (optional)
    getClient: () => client,                         // scoped client → verify connect/set targets
  }),
);

What it does in enforce (all fail-closed):

| operation | behaviour | |---|---| | read / update / delete | ANDs accessibleBy(ability, action)[subject] into where | | field-level read | strips scalar fields the caller may not read from the returned row(s) + included relations, based on permittedFieldsOf(ability, 'read', subject). A read rule without fields grants every scalar (no-op); a rule with fields narrows it (needs dmmf). | | nested include / select (to-many) | ANDs the child's read scope into the nested where (needs dmmf) — throws if it can't be resolved | | create | stamps the tenant field (caller-supplied value can't override it), recursively for nested creates | | nested connect / set / connectOrCreate | pre-verifies the target is in scope via a scoped read (needs getClient), else LINK_DENIED | | raw ops ($queryRaw, aggregateRaw, …) | thrown — can't be scoped; wrap in runUnscoped if intended | | unmanaged model | thrown unless in allowUnmanaged |

The extension reads the ability from AsyncLocalStorage. Wrap each request:

import { authzAls } from '@generazioneai/authz';
await authzAls.run(ctx, async () => client.post.findMany());   // scoped

System work bypasses scoping explicitly:

import { runUnscoped } from '@generazioneai/authz/enforce';
await runUnscoped('nightly-rollup', async () => client.post.count());

Gotcha: the authzAls.run / runUnscoped callback must be async (or return a promise). A sync callback returning a lazy Prisma promise loses the context before the query runs.

3. Build a snapshot (the service that owns roles)

import { buildRulesFromGrants, computePermHash, computeSnapId, RedisSnapshotStore } from '@generazioneai/authz/snapshot';

const grants = /* read the user's roles → [{ action, subject, scope, fields? }] */;
const ctx = { userId, tenantId, orgId, /* …claims */ };
const rules  = buildRulesFromGrants(grants, registry, ctx);
const snapId = computeSnapId(userId, tenantId, Date.now());

await new RedisSnapshotStore(redis).put({
  schemaVersion: 1, snapId, userId, tenantId,
  permHash: computePermHash(rules), builtAt: Date.now(),
  rules, grants,
  claims: { orgId /* …whatever your $ctx paths need downstream */ },
});

Revocation: evict snapshots when roles/permissions change, so the change is immediate (not bounded by the 60s TTL):

const store = new RedisSnapshotStore(redis);
await store.revokeUser(userId);        // on a user's role assignment change
await store.revokeRole('system', roleId); // cascade to all members of a role

4. Verify + hydrate downstream (NestJS + NATS)

import { InternalAuthInterceptor } from '@generazioneai/authz/nest';
import { createInternalJwks, RedisReplayCache } from '@generazioneai/authz/nats';
import { RedisSnapshotStore, hydrateAbility, buildRulesFromGrants } from '@generazioneai/authz/snapshot';

app.useGlobalInterceptors(new InternalAuthInterceptor({
  jwks: createInternalJwks({ url: process.env.AUTHZ_JWKS_URL }),
  replay: new RedisReplayCache(redis),
  serviceName: 'blog-api',
  reflector: app.get(Reflector),
  mode: process.env.AUTHZ_INTERNAL_AUTH_MODE,            // default 'off'
  snapshotStore: new RedisSnapshotStore(redis),
  hydrate: hydrateAbility,
  buildRules: buildRulesFromGrants, registry,            // re-substitute scopes with THIS service's registry
}));

It verifies the internal JWT, fetches the snapshot by its snap claim, replays the claims onto the context, rebuilds the ability with this service's registry (so it scopes subjects the builder doesn't own), and runs the handler inside authzAls.

The signing side (the gateway) uses NatsScopedClientProxy + InternalTokenSigner and serves its public keys at a JWKS endpoint (createInternalJwks fetches it). Keys load via loadSigningKey / generateSigningKeyPair (@generazioneai/authz/nats).

5. Route-level default-deny (optional)

Three intention-revealing decorators on one axis — how much identity a route demands:

import { GlobalAuthzGuard, Authorize, Authenticated, Public } from '@generazioneai/authz/nest';

@Public()                      // no auth at all (login, webhooks, health)
@Get('health') health() {}

@Authenticated()               // any logged-in user — no permission check
@Get('me') me() {}

@Authorize('read', 'Post')     // logged-in AND has the permission (data scoped by the ability)
@Get() findAll() {}

| decorator | login | permission | |---|---|---| | @Public() | ❌ | ❌ | | @Authenticated() | ✅ | ❌ | | @Authorize(action, subject) | ✅ | ✅ |

Register GlobalAuthzGuard as an APP_GUARD with an AbilityResolver (resolves the request's ability), plus your own authn guard first (it enforces login; have it honour PUBLIC_KEY so @Public opts out of both). In enforce, a route with none of the three is denied. Orthogonal concerns (e.g. tenant binding) compose as separate app-provided decorators.


Subpath exports

| import | contents | extra peer deps | |---|---|---| | @generazioneai/authz | defineResource, ResourceRegistry, AuthzContext, authzAls | @casl/* | | @generazioneai/authz/enforce | Prisma extension, runUnscoped, scoped repo | @casl/prisma | | @generazioneai/authz/snapshot | grants→rules, perm-hash, envelope, Redis store + revocation | @casl/prisma, ioredis | | @generazioneai/authz/nats | EdDSA sign/verify, JWKS, replay cache, key loader | jose, ioredis | | @generazioneai/authz/nest | guards, decorators, interceptors, signing proxy | @nestjs/* |

Rollout

Off by default. Flip shadow (measures would-deny, never blocks) on both switches → then enforce. Provision the gateway signing key + the downstream JWKS URL before enabling inter-service auth.

License

MIT