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

pentad

v0.1.0

Published

Typed, explainable, filterable authorization contracts. Control Content from Contact via Contracts Contextually.

Readme

pentad

Typed, explainable, filterable authorization contracts for TypeScript. An implementation of the C5 paradigm: Control Content from Contact via Contracts Contextually.

A contract says who (contact) may do what (action) to which data (content) under what circumstances (context). Every contract answers three questions:

  1. check: may they? A decision, never a bare boolean.
  2. explain: why, or why not? A full pass/fail trace of every rule.
  3. filter: which rows? The same contract compiled into a database query.

Zero runtime dependencies. Runs in Node, the browser, and edge runtimes. Deny by default.

Quickstart

Declare your world once. Everything after this line is inferred.

import { all, any, c5 } from 'pentad'

type World = {
  contact: { userId: string; role: 'admin' | 'editor' | 'viewer'; orgId: string }
  content: {
    post: { id: string; authorId: string; orgId: string; status: 'draft' | 'published'; likes: number }
  }
  context: { channel: 'web' | 'api'; hour: number }
}

const auth = c5<World>()

Write a contract. The lambda runs once at build time against path-recording proxies and produces a plain JSON tree; it never sees real data.

const editPost = auth.contract('edit', 'post').when(({ contact, content, context }) =>
  any(
    contact.role.eq('admin'),
    all(
      content.authorId.eq(contact.userId),   // cross-block reference, type-checked
      content.status.neq('published'),
      context.channel.oneOf(['web']),
    ),
  ),
)

Operators are offered per field type: contact.role. autocompletes eq / neq / oneOf / notOneOf / matches, while number fields add gt / gte / lt / lte / between. Typos (contact.rol), wrong literals (status.eq('archived')), and wrong-type operators (role.gt(5)) are compile errors. A field literally named eq is reachable via the escape hatch content.$field('eq').

Question 1: check

const decision = editPost.check({
  contact: { userId: 'u_1', role: 'editor', orgId: 'o_1' },
  content: { id: 'p_9', authorId: 'u_1', orgId: 'o_1', status: 'published', likes: 3 },
  context: { channel: 'web', hour: 14 },
})

decision.allowed  // false
decision.denied   // true, the literal-typed complement; no more !decision.allowed

allowed and denied are halves of a discriminated union, so TypeScript narrows on either one, and only denied decisions carry a reason. For the most common call site there is orThrow():

editPost.check(payload).orThrow()                          // throws ForbiddenError(reason) on deny
editPost.check(payload).orThrow(() => new HttpError(403))  // or your own error

Question 2: explain

console.log(decision.explain())
// ✗ edit post
//   ✗ any
//     ✗ contact.role equals "admin" (found "editor")
//     ✗ all
//       ✓ content.authorId equals contact.userId ("u_1")
//       ✗ content.status not "published" (found "published")
//       ✓ context.channel in ["web"]

decision.reason is the one-line version, taken from the nearest-miss branch: the branch that came closest to passing, since that is the most actionable explanation.

Question 3: filter

Bind contact and context, leave content unknown, and the tree partially evaluates into a residual predicate over content alone, compiled to your target:

import { prismaAdapter } from 'pentad/adapters/prisma'
import { memoryAdapter } from 'pentad/adapters/memory'

const where = editPost.filter({ contact: me, context: { channel: 'web', hour: 14 } }, prismaAdapter())
// { AND: [{ authorId: 'u_1' }, { status: { not: 'published' } }] }
const editable = await prisma.post.findMany({ where })

const keep = editPost.filter({ contact: me, context }, memoryAdapter<Post>())
const visible = posts.filter(keep)

The library's honesty invariant: filtering then fetching returns exactly the rows that check would allow, and the test suite enforces it.

Context providers

Register how ambient context is resolved once, and it disappears from every call site:

import { time } from 'pentad/ambient'

const auth = c5<World, { req: Request }>().provide('context', {
  hour: time.hour(),
  channel: ({ req }) => (req.headers.get('x-client') === 'api' ? 'api' : 'web'),
})

// context is no longer accepted in the payload; supplying it is a compile error
const decision = editPost.check({ contact: me, content: post }, { req })

Provided fields cannot be spoofed: providers overwrite anything a caller smuggles in at runtime, and the types reject it at compile time. Providers may be async; when any registered provider returns a promise, check and filter are typed as returning promises.

Control: the contract registry

const control = auth.control([editPost, deletePost, readPost])

control.check('edit', 'post', payload)     // routed by action and resource
control.check('delete', 'post', payload)   // no contract registered: denied, reason "no contract"

Multiple contracts for the same action and resource are OR'd: first allow wins.

Testing

Policies are production code. The matrix harness ships in the box:

import { matrix } from 'pentad/testing'

matrix(editPost)
  .context({ hour: 14, channel: 'web' })
  .allow('author edits own draft', { contact: author, content: draft })
  .deny('published is locked', { contact: author, content: published })
  .deny('stranger denied', { contact: stranger, content: draft })
  .run()   // a wrong expectation throws with the full explain trace

The matrix bypasses context providers on purpose: spoofing context is the point of a test.

Serialization

A contract is plain JSON, so it can be stored anywhere and rehydrated with validation:

const saved = JSON.stringify(editPost)
// {"on":"edit","to":"post","when":{"any":[{"op":"eq","left":{"ref":"contact.role"},"right":"admin"},...]}}

const revived = auth.fromJSON(JSON.parse(saved))   // throws InvalidContractError on unknown operators

References are explicitly tagged ({"ref": "contact.userId"}), so a literal and a reference can never be confused.

Design notes

  • One operator table (OPERATORS) is the single source of truth. The evaluator, the explainer, and every filter adapter consume it, and adapters implement Record<Op, ...> which the compiler checks for exhaustiveness. An operator cannot exist without a complete implementation everywhere.
  • Deny by default: a missing attribute fails its rule, an empty any fails, an unregistered action and resource pair fails.
  • The evaluator does not short-circuit, so the trace reports every branch. Policy trees are small; trace fidelity is worth more than the skipped comparisons.

Package map

  • pentad: core. Builder, evaluator, decisions, explain, partial evaluation, control.
  • pentad/adapters/memory: residual tree to a row predicate.
  • pentad/adapters/prisma: residual tree to a Prisma where clause.
  • pentad/ambient: clock-injectable time providers.
  • pentad/testing: the matrix harness.

Roadmap

  • Drizzle and raw SQL filter adapters.
  • Property-based tests over the evaluator and the filter round-trip invariant.
  • An optional standalone HTTP decision service (@pentad/server) with ad-hoc and saved-contract checks.
  • Contract versioning and decision audit logs.