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

@aether-zone/organon

v0.5.0

Published

Shared NestJS building blocks for aether-zone: RFC 9457 problem responses, the pistis resource-server contract and guard, and RabbitMQ events.

Readme

@aether-zone/organon

A NestJS library.

pnpm add @aether-zone/organon
import { Module } from '@nestjs/common';
import { OrganonModule } from '@aether-zone/organon';

@Module({ imports: [OrganonModule] })
export class AppModule {}

Errors as Problem JSON

ProblemException is an error that already knows how it renders; ProblemExceptionFilter renders every failure — not just that one — as an RFC 9457 problem document, served as application/problem+json.

import { APP_FILTER } from '@nestjs/core';
import { ProblemExceptionFilter } from '@aether-zone/organon';

@Module({
  providers: [{ provide: APP_FILTER, useClass: ProblemExceptionFilter }],
})
export class AppModule {}
throw new ProblemException({
  status: HttpStatus.CONFLICT,
  type: 'https://example.com/probs/slug-taken',
  title: 'That slug is already in use',
  detail: `"${slug}" belongs to another organization.`,
  extensions: { slug },
});
{
  "type": "https://example.com/probs/slug-taken",
  "title": "That slug is already in use",
  "status": 409,
  "detail": "\"acme\" belongs to another organization.",
  "instance": "/organizations/acme",
  "slug": "acme"
}

Four behaviours are deliberate:

  • An unexpected error never reaches the client. Anything that is not an HttpException becomes a bare 500 whose message is dropped — those messages name queries, paths and drivers. The stack is logged instead, so dropping it does not mean losing it.
  • ValidationPipe's messages become an errors extension, not a joined sentence, because a client marking up form fields needs them apart.
  • A blank type gets the status reason phrase as its title, which is what RFC 9457 §4.2.1 asks for.
  • An extension may not shadow a standard member. extensions: { status } throws rather than silently producing a document whose status disagrees with the response code.

One import

OrganonModule.forRoot() wires configuration, logging, health and the problem filter together.

@Module({
  imports: [
    OrganonModule.forRoot({
      config: { schema: envSchema },
      logging: { base: { service: 'akouo' } },
      health: { indicators: [DatabaseHealth] },
    }),
  ],
})
export class AppModule {}

Each part is registrable on its own, and this changes none of their behaviour. It exists because two pairs of them only work properly when they know about each other:

  • The problem filter reports the request id the logger issued. A 500 deliberately tells the client nothing, so that id is the only route from a reported failure to the stack trace explaining it.
  • The health probes are excluded from the request log, derived from the health path so it stays right when the path is moved. An orchestrator polls them every few seconds; left in, they are most of the log.

config is omitted by default — there is no schema a library could supply. health, logging and problem are on; pass false to any of them to leave that part out.

OrganonModule.forRoot({ problem: false });   // keep your own error rendering
OrganonModule.forRoot({ health: false });    // no probes; nothing is excluded
                                             // from the log either

Configuration

AppConfigModule loads the environment and validates it against a schema the application supplies.

import { AppConfigModule, ENV, baseEnvSchema, booleanFromString } from '@aether-zone/organon';

export const envSchema = baseEnvSchema.extend({
  DATABASE_URL: z.string().min(1),
  DEBUG_MODE: booleanFromString.default(false),
});
export type Env = z.infer<typeof envSchema>;

@Module({ imports: [AppConfigModule.forRoot({ schema: envSchema })] })
export class AppModule {}

The schema is a parameter, not something this module owns. A library cannot know what an application's environment looks like, and a schema fixed here would name variables that mean nothing to most consumers — worse, require them, which is a boot failure for everyone who does not happen to set them. baseEnvSchema is deliberately small — NODE_ENV, PORT and LOG_LEVEL, the three every service has; extend it with your own.

LOG_LEVEL is validated against the levels the logger knows, so a typo fails the boot rather than being ignored. It is optional rather than defaulted: JsonLogger already falls back to log under NODE_ENV=production and debug elsewhere, and a default here would either restate that rule or quietly override it — silencing debug output in development because a variable was unset. Pass it straight through:

const app = await NestFactory.create(AppModule, {
  logger: new JsonLogger({ level: env.LOG_LEVEL, base: { service: 'akouo' } }),
});

Case and surrounding space are forgiven (LOG_LEVEL=DEBUG works), but the names are Nest's, so the middle one is log — info is refused, with the valid options named.

Inject the whole validated environment rather than fishing keys out of ConfigService:

constructor(@Inject(ENV) private readonly env: Env) {}

ENV is the parsed object, so defaults and coercions are already applied — env.PORT is a number, and DEBUG_MODE=false is false rather than a truthy string. EnvService<Env> still narrows ConfigService where you want it, but note it is a type alias and so not a DI token: name ConfigService in the @Inject as well.

An invalid environment fails the boot, listing every problem:

Invalid environment configuration:
  - NODE_ENV: Invalid option: expected one of "development"|"test"|"production"
  - PORT: Invalid input: expected number, received NaN

NestFactory defaults to abortOnError: true, which logs that and exits; with { logger: false } there is nothing left to print it and you get a silent exit

  1. Pass abortOnError: false to handle the rejection yourself.

Accepting pistis tokens

PistisAuthModule makes a service a resource server for the pistis authorization server: it accepts the bearer tokens pistis minted, and does nothing else.

import { PistisAuthModule, jwksUriFor } from '@aether-zone/organon';

const issuer = 'https://pistis.example.com';

@Module({
  imports: [
    PistisAuthModule.register({ issuer, audience: issuer, jwksUri: jwksUriFor(issuer) }),
  ],
})
export class AppModule {}

registerAsync takes the same options from a factory, which is what reading them out of the validated environment needs:

PistisAuthModule.registerAsync({
  inject: [ConfigService],
  useFactory: (config: EnvService<Env>) => {
    const issuer = config.get('OAUTH_ISSUER', { infer: true });

    return {
      issuer,
      audience: config.get('OAUTH_AUDIENCE', { infer: true }) ?? issuer,
      jwksUri: config.get('OAUTH_JWKS_URI', { infer: true }) ?? jwksUriFor(issuer),
    };
  },
});

| Option | | | --- | --- | | issuer | The iss every token must carry — pistis's public origin, not this service's. A token from anywhere else is refused even if its signature is good. | | audience | The aud every token must carry. pistis defaults this to its issuer. | | jwksUri | Where pistis publishes its public signing keys. jwksUriFor(issuer) derives it the way RFC 8414 lays it out, so a deployment normally configures the issuer alone. | | tokenType | The typ the token header must carry. Defaults to at+jwt. |

It is not part of OrganonModule.forRoot(). Health, logging and problem rendering all have a default worth having; an issuer does not, and a service that is not a resource server should not be made to name one.

There is nothing to sign in to. A resource server issues no tokens, stores no passwords and keeps no user table. Signing in happens in whatever web app runs the authorization code flow against pistis; what arrives here is the result. The only identity worth keeping is the token's sub — names and email addresses live in pistis.

Every route requires a token, because the module registers its guard as an APP_GUARD. @Public() opts one out:

@Public()
@Get('version')
version() {
  return { version: process.env.APP_VERSION };
}

@CurrentUser() hands the handler what the token resolved to:

@Get('me')
me(@CurrentUser() principal: Principal) {
  return { id: principal.id, scopes: principal.scopes };
}
interface Principal {
  id: string;                    // the token's `sub`
  clientId: string;              // which registered client it was issued to
  scopes: string[];              // already split out of the space-delimited claim
  organizations: Record<string, OrganizationMembershipClaim>;
}

hasScopes(principal, 'meetings:write') answers the scope question; parseScope and formatScope are there for anything that has to read or write the claim itself.

Five things are deliberate:

  • Validation is offline. The signature is checked against pistis's published JWKS and nothing else is asked of it, so a request costs no round trip to the authorization server. The cost is that a revoked token stays good until it expires — pistis keeps a row per jti precisely so it can answer that, through /oauth/introspect, if revocation ever needs to take effect sooner.
  • RS256 is pinned rather than read from the token's own alg. That is what closes alg: none and the RSA-to-HMAC confusion attack, and it is the same rule pistis applies when verifying.
  • The header's typ is checked too. A pistis session token is signed by the same key, so the signature alone does not tell the two apart. Refusing anything but at+jwt means a widened audience cannot quietly turn a session into an access token.
  • The default is closed. A new controller is authenticated because nobody did anything, which is the only default worth having.
  • Signing keys are cached by kid and refetched when a token names an unknown one, which makes key rotation a non-event: the first token signed by a new key misses, triggers one fetch, and every later token hits. An unknown kid cannot be used to hammer pistis — there is a floor between refetches.

Acting in an organization

A token carries the subject's memberships in its orgs claim, so deciding whether a request may act in the organization it names takes no query and no call back to pistis.

@Controller('organizations/:organizationId/meetings')
@UseGuards(OrganizationGuard)
export class MeetingController {

  @Get()
  list(@CurrentActor() actor: Actor) {
    return this.meetings.list(actor);
  }

  @Delete(':id')
  @RequireRole('admin')
  remove(@CurrentActor() actor: Actor, @Param('id') id: string) {
    return this.meetings.remove(actor, id);
  }
}

OrganizationGuard reads the organization out of the path, refuses a caller who may not act in it, and leaves an Actor for @CurrentActor(). @RequireRole() raises the bar from plain membership; absent, membership is what is required.

interface Actor extends Principal {
  organizationId: string;      // the organization this request named
  role: MembershipRole;        // 'owner' | 'admin' | 'member'
  organizationName: string;    // display only, and as stale as the token
}

Principal says who the caller is and everywhere they could act; an Actor narrows that to the one organization at hand. A service that takes an Actor rather than a Principal and an id cannot filter a query by the wrong organization: there is only one to reach for.

The guard injects nothing but Reflector, so a module declaring an organization-scoped controller imports nothing to use it — @UseGuards(OrganizationGuard) is the whole of the wiring.

OrganizationGuard expects :organizationId. For a service that names it something else, organizationGuardFor builds the same guard around another parameter — once, at module scope, because Nest caches guard instances per class and calling it inside @UseGuards() would make a new one per controller:

const TenantGuard = organizationGuardFor('tenantId');

Underneath is actorIn(principal, organizationId, atLeast?), which is the whole decision without the request: it answers null when the caller may not act there, and an Actor when they may. Reach for it directly outside a controller — resolving an organization from a message body rather than a path, say. Where the organization id comes from is deliberately the caller's problem, which is what lets the guard above be the only piece that knows about URLs.

Three things are deliberate:

  • An unknown organization and someone else's give the same 403. A 404 for one and a 403 for the other would answer "does this organization exist" for anyone who cared to ask — and for the same reason, actorIn returns the same null whether the caller is not a member or merely not senior enough. Use membershipIn and roleIn where you genuinely need to tell them apart.
  • The claim is as stale as the token. Someone removed from an organization keeps access until their client refreshes. pistis re-resolves the map on every issue, refreshes included, so a refresh is what catches a client up. That is the price of not asking pistis on every request, and it is worth naming rather than discovering.
  • organizationName is display only. A rename in pistis is invisible to an already-issued token, while role is the fact every access decision turns on. Never key anything on the name.

Peer dependencies

All required, none optional: @nestjs/common, @nestjs/core, @nestjs/config, @nestjs/passport, passport, passport-jwt, @golevelup/nestjs-rabbitmq, reflect-metadata, rxjs and zod.

The passport three and the RabbitMQ client are required for the same reason: the root barrel re-exports auth/ and messaging/, which import them as values, so requiring this package requires them — even for a consumer that only wants a problem filter, and even for one that never touches a queue. Splitting the entry points is what would buy that back, and the cost grows with each part that has a client library behind it.

@golevelup/nestjs-rabbitmq asks for @nestjs/common and @nestjs/core ^11.1.21 where this package asks for ^11.0.1. A consumer on an earlier 11.x will see an unmet-peer warning from it.

Events over RabbitMQ

RabbitMqModule wraps @golevelup/nestjs-rabbitmq rather than replacing it: @RabbitSubscribe, AmqpConnection and the rest are that package's and are used directly.

RabbitMqModule.registerAsync({
  inject: [ENV],
  useFactory: (env: Env) => ({ uri: env.RABBITMQ_URI }),
});

| Option | | | --- | --- | | uri | amqp://user:pass@host:5672, or a vhost URL. | | exchange | The topic exchange events go to. aether-zone by default. | | prefetch | Messages a consumer holds unacknowledged at once. 1. | | connectTimeoutMs | Wait this long for the broker before finishing the boot; false to start anyway and connect in the background. |

Publish with the routing key and the caller's token:

await this.events.publish('recording.stored', { recordingId }, accessToken);

Subscribe with the package's own decorator:

@RabbitSubscribe({
  exchange: 'aether-zone',
  routingKey: 'recording.*',
  queue: 'transcription.recordings',
})
async onRecording(event: RecordingStoredEvent) {}

Name the queue. An anonymous one is exclusive and vanishes with the process, so a restart loses whatever arrived meanwhile.

The envelope

Every event carries id, occurredAt and an accessToken, filled in by EventPublisher so no publisher has to remember them:

interface RabbitEvent {
  id: string;          // unique per publish; survives a redelivery
  occurredAt: string;  // when it happened, not when it was delivered
  accessToken: string; // whoever caused it
}

The token is there because work that starts from an event has no request to borrow one from. Without it a consumer calling another service must act as itself, which loses which person the work was for and needs an authority of its own for something a person asked for.

Three things follow, and none are theoretical:

  • A token in a message is a credential in a queue. It is written to the broker's disk for a durable queue, readable by anything that can read the queue, and lands in the dead-letter queue if the consumer keeps failing. Broker access is token access; grant it accordingly.
  • It expires. A message that waits — a backlog, a retry, an outage — can be delivered with a token no longer worth presenting. isExpired(event) answers that, reading exp without verifying the signature, which is all that is needed to decide whether presenting it is worth trying. A consumer that finds one should fall back to its own credentials or give up, not retry forever.
  • It is not proof of anything by itself. A consumer acting on its claims must verify it, exactly as it would a token from an HTTP header.

persistent is set on every publish, so an event outlives a broker restart — the point of sending it rather than doing the work inline. Delivery is at-least-once: id is what a consumer deduplicates on.

Health

@Module({ imports: [HealthModule.forRoot()] })
export class AppModule {}

| Route | Question | Checks dependencies | | --- | --- | --- | | GET /health/live | Is the process running? | no | | GET /health/ready | Can it serve traffic? | yes | | GET /health | — | yes, same as /ready |

The path is configurable, and replaces the default rather than adding to it:

HealthModule.forRoot({ path: 'internal/health' });
// -> /internal/health, /internal/health/live, /internal/health/ready

@Controller() is evaluated when a class is defined, so the decorator is applied to a fresh subclass of HealthControllerBase per registration — createHealthController(path), exported if you want to mount it yourself. To keep health under a wider prefix instead, leave path alone and use Nest's RouterModule.register([{ path: 'internal', module: HealthModule }]).

Liveness deliberately checks nothing. A liveness probe answers "should this process be restarted", and restarting a healthy process because its database went down turns one outage into two — the restarts remove capacity exactly when the dependency recovers and the load arrives. Dependencies belong in readiness, which takes the instance out of the load balancer and puts it back afterwards.

Register indicators for readiness:

@Injectable()
class DatabaseHealth implements HealthIndicator {
  readonly name = 'database';
  constructor(private readonly db: DataSource) {}

  async check(): Promise<HealthCheckResult> {
    await this.db.query('select 1');
    return { status: 'up' };
  }
}

HealthModule.forRoot({
  imports: [DatabaseModule],
  indicators: [DatabaseHealth],
  info: { service: 'akouo', version: process.env.APP_VERSION },
});

Readiness answers 200 or 503 with the same body either way:

{ "status": "down", "uptime": 41,
  "info": { "service": "akouo", "version": "1.2.3" },
  "checks": { "database": { "status": "up" },
              "cache": { "status": "down", "error": "connection refused" } } }

Four things are deliberate:

  • Every indicator is bounded by a timeout (3s by default). A check that hangs would hang the endpoint, and an endpoint that never answers reads as a liveness failure — so the process gets restarted for a fault in something it merely talks to.
  • An indicator that throws is reported, not propagated. One broken check marks itself down and leaves the rest of the report intact.
  • The report is returned, not thrown, so success and failure have the same shape. It does not go through ProblemExceptionFilter: a 503 from readiness is an expected operational signal rather than an error, and the report's own status field would collide with the problem document's.
  • The routes are @Public(), so a global token guard does not apply — an orchestrator has no credentials, and a health endpoint behind authentication reports every instance as unhealthy.

Check details name the failing dependency, so keep these routes off the public internet. Pair with the logger so the probes do not fill the log: LoggerModule.forRoot({ ignorePaths: ['/health', '/health/live', '/health/ready'] }).

Logging

LoggerModule gives every request an id, logs a line per request, and makes that id available to anything the request goes on to do.

import { JsonLogger, LoggerModule } from '@aether-zone/organon';

@Module({
  imports: [
    LoggerModule.forRoot({
      base: { service: 'akouo' },
      ignorePaths: ['/health'],
    }),
  ],
})
export class AppModule {}

// The application logger has to be set before the app exists, so no module
// can do it. Give it the same options.
const app = await NestFactory.create(AppModule, {
  logger: new JsonLogger({ base: { service: 'akouo' } }),
});
{"service":"akouo","level":"log","time":"...","message":"deep inside a service",
 "context":"DeepService","requestId":"c1653b88-…"}
{"service":"akouo","level":"log","time":"...","message":"GET /ok 200 0.6ms",
 "context":"Request","requestId":"c1653b88-…"}

The id is carried in an AsyncLocalStorage, so a log written inside a service that knows nothing about it is still attributed to the request that caused it — without threading an argument through every function that might one day log.

It pairs with ProblemExceptionFilter. A failure's problem document carries the same requestId, and it is returned in the x-request-id response header:

{ "type": "about:blank", "title": "Internal Server Error", "status": 500,
  "instance": "/boom", "requestId": "54098bd3-…" }

An unexpected error deliberately tells the client nothing about what went wrong, so that id is the only way to get from "it failed" to the stack trace that says why. Searching the logs for it finds both the request line and the filter's record of the exception.

Four things are deliberate:

  • Middleware, not an interceptor, so the context is open before guards run. An interceptor would leave a rejected authentication outside it.
  • No request body, query string or headers are logged. Bodies carry passwords and Authorization carries the credential itself; the request line logs the path with the query string stripped.
  • The level follows the status — 5xx error, 4xx warn, otherwise log. A log where everything is one level cannot be filtered.
  • An inbound x-request-id is ignored by default. Behind a gateway that sets it, turn on trustInboundRequestId to make one id span services; exposed to the internet, leave it off — an id a caller picks is one they can repeat, colliding their requests with someone else's in your logs. When trusted it is still length-capped and character-checked before being written anywhere.

Ships ESM and CommonJS: a Nest application generated today is still CommonJS, so an ESM-only build would be unusable by the most likely consumer. @nestjs/common, @nestjs/core, reflect-metadata and rxjs are peer dependencies — the library must run against the application's Nest, not a second copy of it.