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

@nyuro/rbac

v0.3.0

Published

Nyuro authorisation: permission catalogue, snapshot decider, route policy DSL and table matcher (.), Express policy router and introspection (./express).

Readme

@nyuro/rbac

Authorisation for Nyuro products: a permission catalogue, a fail-closed snapshot decider, a route policy DSL with an exact table matcher, and Express bindings.

| Import | For | |---|---| | @nyuro/rbac | Framework-neutral core: catalogue, evaluation, decider, policy DSL, route policy table | | @nyuro/rbac/express | policyRouter, policy middleware, edge gate, stack introspection (Express 4 or 5) |

express is an optional peer dependency. The /express entry never imports it: it works on structural types, so its declarations need neither express nor @types/express.

Catalogue

import { defineCatalogue } from '@nyuro/rbac'

export const catalogue = defineCatalogue([
    { name: 'chatflows:view',   category: 'chatflows', scope: 'workspace', apiKeyAllowed: true },
    { name: 'chatflows:update', category: 'chatflows', scope: 'workspace', apiKeyAllowed: true },
    { name: 'evaluations:run',  category: 'evaluations', scope: 'workspace', apiKeyAllowed: true, feature: 'evaluations' },
    { name: 'workspace:update', category: 'workspace', scope: 'workspace', apiKeyAllowed: false }
])
await platform.raw.PUT('/authz/products/{product}/permissions', {
    params: { path: { product: 'flow' } },
    body: { permissions: catalogue.toDefinitions('flow') }      // names become flow:chatflows:view
})

Rules, applied whenever a catalogue is supplied:

  • Implies. A non-view permission implies <category>:view when that permission exists. implies: [...] overrides it ([] implies nothing); closure is transitive. Unknown targets throw.
  • API keys. A permission with apiKeyAllowed: false is never satisfied for a kind: 'apiKey' principal, even when the key holds it, and contributes nothing through implies.
  • Features. A permission with feature counts only when principal.features[feature] === true.
  • Unknown names are never satisfied (a typo denies rather than allows).

Without a catalogue, names compare literally.

Deciding

import { createSnapshotDecider } from '@nyuro/rbac'
import { createPlatformClient } from '@nyuro/sdk/platform'

const decider = createSnapshotDecider({ product: 'flow', catalogue, remote: createPlatformClient({ ... }) })
await decider.decide(principal, ['chatflows:create', 'chatflows:update'], 'any')
await decider.decide(principal, ['chatflows:view'], 'all', { resource: { type: 'chatflow', id } })
  • Decisions are local, from principal.permissions (the snapshot identity resolve returns). Snapshot entries may be bare (chatflows:view) or namespaced (flow:chatflows:view); entries for other products are ignored.
  • A check that names a resource goes to POST /platform/v1/authz/decide (resource grants live there) with names namespaced for the product, after the local hard rules (unknown name, API-key restriction, feature gate) pass. Without remote, resource checks are decided locally.
  • Fail closed. decide never throws and never allows on error: a failure is { allowed: false, unavailable: true } (reported to onError). HTTP layers answer 503 for it.
  • decide(principal, required, mode) has the call shape of PermissionPort.decide in @nyuro/flow-ports, and the flow-ports and contract Principal types satisfy RbacPrincipal.

Route policies

import { pub, flow, session, perm, createPolicyTable } from '@nyuro/rbac'

pub()                                                    // anyone
flow()                                                   // anonymous at the edge; the controller enforces the flow's own policy
session()                                                // any authenticated principal
perm('tools:view')                                       // all of these (default)
perm('chatflows:create', 'chatflows:update', { mode: 'any' })
perm('chatflows:view', { resource: (req) => ({ type: 'chatflow', id: req.params.id }) })

const table = createPolicyTable([
    { method: 'GET', path: '/api/v1/ping', policy: pub() },
    { method: 'POST', path: '/api/v1/prediction/:id', policy: flow() },
    { method: 'GET', path: ['/api/v1/chatflows', '/api/v1/chatflows/:id'], policy: perm('chatflows:view') }
])
table.isAnonymousAllowed('GET', '/api/v1/ping')          // true
table.isAnonymousAllowed('GET', '/api/v1/ping/')         // false: one spelling per path

The matcher is exact and segment-wise (no regular expressions):

  • No prefix matching: /ping does not match /ping/x or /pingx.
  • Case-sensitive. Pair it with app.set('case sensitive routing', true).
  • Requests with .., ., //, a trailing slash, an encoded dot/slash/backslash or a backslash never match, so they are never anonymous. Patterns with those throw at construction.
  • Pattern syntax: literals, :name, :name? (last segment), *name (last segment, one or more segments). Regex params and RegExp paths are rejected.
  • ALL rows match every method; HEAD also matches GET rows (as Express routes them).
  • The most specific row wins match() (literal > param > optional > wildcard). isAnonymousAllowed is true only when a row matches and every matching row is pub or flow.
  • Duplicate (method, pattern) rows throw unless their policies are equivalent.

perm() validates names at definition time and rejects commas, so checkAnyPermission('a,b') becomes perm('a', 'b', { mode: 'any' }).

Express

import express from 'express'
import { createPolicyMiddleware, createEdgeGate, policyRouter, listRoutes, policyTableFromRoutes, undeclaredRoutes } from '@nyuro/rbac/express'

const enforce = createPolicyMiddleware({ decider, getPrincipal: (req) => req.principal })

const router = policyRouter(express.Router(), { enforce })
router.get(['/', '/:id'], perm('tools:view'), toolsController.get)
router.get('/x', toolsController.get)                    // throws: no policy declared

const api = policyRouter(express.Router(), { enforce })
api.use('/tools', router)                                // mount path recorded for introspection

const table = policyTableFromRoutes(listRoutes(api, { prefix: '/api/v1' }))
app.set('case sensitive routing', true)
app.use(identify)                                        // sets req.principal or 401s bad credentials
app.use(createEdgeGate({ table, getPrincipal: (req) => req.principal }))
app.use('/api/v1', api)
  • policyRouter wraps every HTTP verb method and all on the instance: the policy is the first argument after the path and is mandatory; route() is disabled; paths must be canonical strings or arrays of them. The enforcing middleware runs before the handlers.
  • The policy middleware: pub/flow pass; session needs a principal (401); perm needs a principal (401) and an allowing decision (403); a thrown or unavailable decision is 503. Bodies are { error, code }; override with respond. onDecision observes perm decisions.
  • createEdgeGate lets a request without a principal through only when the table allows the exact (method, path) anonymously; it reads req.originalUrl without the query by default.
  • listRoutes(router, { prefix }) walks the Express 4/5 stack and returns every { method, path, policy, mountKnown }. Handlers registered outside a policy router have policy: undefined; nested routers mounted with a plain use have mountKnown: false. A route-classification test asserts undeclaredRoutes(listRoutes(...)) is empty and can snapshot formatRoutes(...).