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

advance-authz

v1.0.0

Published

Lightweight, secure authorization engine for Node.js using TOON policy files. Zero eval(), TypeScript-first, deny-by-default.

Downloads

12

Readme

advance-authz

A lightweight, secure authorization engine for Node.js using TOON policy files.

CI Tests Coverage npm License


Why advance-authz?

Authorization logic scattered across application code looks like this:

if (user.role === 'admin') { ... }
if (user.id === listing.owner_id) { ... }
if (user.permissions.includes('edit_listing')) { ... }

Over time this becomes unmaintainable. advance-authz moves all authorization rules into a single policy file so your application only ever asks one question:

authz.can(user, 'edit', 'listing', listing)

Features

  • TOON v2 policy language — readable, human-auditable .toon files
  • Role hierarchy — super_admin extends admin extends editor
  • Deny rules — explicit effect: deny always overrides any allow
  • Conditions — AND, OR, NOT with full precedence rules
  • Zero eval() — conditions evaluated by pure AST tree-walk
  • Prototype pollution protection — user/resource inputs are deep-cloned and frozen
  • Path traversal protection — load() and include both validate paths against process.cwd()
  • TypeScript-first — full .d.ts, zero any in public API
  • Dual ESM + CJS — works with import and require
  • Audit callback — structured decision logging for compliance
  • Deny by default — if no rule matches, access is denied

Installation

npm install advance-authz

Requirements: Node.js 18 or later.


Quick start

1. Create a policy file

# policies/authz.toon

role_hierarchy
  super_admin extends admin
  admin extends editor
  editor extends viewer
end

rule
  role admin
  action *
  resource *
  effect allow
end

rule
  role broker
  action edit
  resource listing
  effect allow
  condition resource.owner_id == user.id AND resource.status == "draft"
end

rule
  role user
  action view
  resource listing
  effect allow
  condition resource.status == "public" OR resource.owner_id == user.id
end

2. Load and check permissions

import { Authz } from 'advance-authz'

const authz = new Authz()
await authz.loadAsync('./policies/authz.toon')

// Simple check
const allowed = authz.can(user, 'edit', 'listing', listing)

// Detailed explanation (for debugging)
const result = authz.explain(user, 'edit', 'listing', listing)
// {
//   allowed: true,
//   reason: 'allow-rule-matched',
//   matchedRole: 'broker',
//   matchedAction: 'edit',
//   matchedResource: 'listing',
//   conditionResult: true,
//   durationMs: 0.12
// }

3. Protect Express routes

app.put('/listings/:id', async (req, res) => {
  if (!authz.can(req.user, 'edit', 'listing', req.listing)) {
    return res.status(403).json({ error: 'Access denied' })
  }
  // proceed
})

API reference

new Authz(options?)

const authz = new Authz({
  // Called after every authorization decision. Errors are swallowed.
  audit: (record: AuditRecord) => logger.info(record),

  // Maximum nesting depth for user/resource objects (default: 10)
  maxContextDepth: 10,
})

authz.load(filePath)

Synchronously load and compile a .toon policy file.

authz.load('./policies/authz.toon')

Throws PathSafetyError if the path resolves outside process.cwd(). Throws ParseError or CompileError on invalid policy content.


authz.loadAsync(filePath)

Asynchronous version of load(). Preferred in production server code.

await authz.loadAsync('./policies/authz.toon')

authz.can(user, action, resource, resourceObject?, extraCtx?)

Returns true if the user is permitted to perform the action. Deny rules always override allow rules. Returns false if no policy is loaded.

authz.can(user, 'edit', 'listing', listing)
authz.can(user, 'view', 'report', report, { tenantId: req.tenantId })

| Parameter | Type | Required | Description | |---|---|---|---| | user | User | Yes | Authenticated subject — must have id and roles | | action | string | Yes | The action being performed | | resource | string | Yes | The resource type name | | resourceObject | object | No | The actual resource (used in conditions) | | extraCtx | object | No | Extra context — available as ctx.* in conditions |


authz.explain(user, action, resource, resourceObject?, extraCtx?)

Returns a detailed AuthzResult explaining the decision. Use for debugging and audit logging. Use can() in hot paths.

const result = authz.explain(user, 'edit', 'listing', listing)
// AuthzResult:
// {
//   allowed: boolean
//   reason: 'allow-rule-matched' | 'deny-rule-matched' | 'condition-failed'
//         | 'no-matching-rule' | 'wildcard-matched'
//   matchedRole?: string
//   matchedAction?: string
//   matchedResource?: string
//   conditionResult?: boolean
//   durationMs: number
// }

authz.validate(filePath)

Validates a .toon file without loading it into the engine. Never throws (except for PathSafetyError — path safety violations are always re-thrown).

const result = authz.validate('./policies/authz.toon')
// { valid: true, errors: [] }
// { valid: false, errors: [{ message, line, column }] }

Policy language (TOON v2)

Rule syntax

rule
  role    <role-name>
  action  <action-name | *>
  resource <resource-name | *>
  effect  <allow | deny>          # optional — defaults to allow
  condition <expr>                 # optional
end

All four fields (role, action, resource) are required. effect and condition are optional.


Effects

# Allow rule (default when effect is omitted)
rule
  role admin
  action *
  resource *
  effect allow
end

# Deny rule — overrides any matching allow rule
rule
  role admin
  action delete
  resource archived_listing
  effect deny
end

Deny always wins. If a deny rule matches, access is denied regardless of any allow rules that also match.


Conditions

Conditions reference three root variables:

| Variable | Source | |---|---| | user | The User object passed to can() | | resource | The resourceObject (4th argument to can()) | | ctx | The extraCtx (5th argument to can()) |

# Ownership check
condition resource.owner_id == user.id

# Combined with AND
condition resource.owner_id == user.id AND resource.status == "draft"

# Combined with OR
condition resource.status == "public" OR resource.owner_id == user.id

# Negation
condition NOT resource.archived == true

# Parentheses for grouping
condition (resource.status == "public" OR resource.owner_id == user.id) AND NOT resource.suspended == true

Operator precedence (tightest to loosest):

  1. ( ) — grouping
  2. == != > >= < <= — comparison
  3. NOT — unary negation
  4. AND — logical and
  5. OR — logical or

Comparison operators: == != > >= < <= All use strict equality — no JavaScript type coercion.


Role hierarchy

role_hierarchy
  super_admin extends admin
  admin extends editor
  editor extends viewer
end

super_admin inherits all rules of admin, editor, and viewer. Inheritance is fully expanded at compile time — zero runtime cost. Cycles are detected and throw a CompileError.


Include directive

Split large policies across multiple files:

include "./policies/listings.toon"
include "./policies/users.toon"

Paths are resolved relative to the file containing the include, and every resolved include path is also validated against process.cwd() — an include cannot be used to read a file outside the project root. Circular includes are detected and throw a CompileError.

Windows note: string literals treat \ as an escape-sequence introducer (same as JSON/JS), so a raw Windows path like include "C:\Users\me\policy.toon" will fail to parse. Use forward slashes instead — include "C:/Users/me/policy.toon" — the same convention used by require()/import specifiers.


Comments

# This is a comment
rule
  role admin   # inline comment
  action *
  resource *
end

TypeScript types

import type {
  User,
  AuthzOptions,
  AuthzResult,
  AuditRecord,
  ValidationResult,
  PolicyError,
  DecisionReason,
} from 'advance-authz'

// User shape
interface User {
  id: string | number
  roles: readonly string[]
  [key: string]: unknown  // any additional fields accessible in conditions
}

// Decision reasons
type DecisionReason =
  | 'allow-rule-matched'
  | 'deny-rule-matched'
  | 'condition-failed'
  | 'no-matching-rule'
  | 'wildcard-matched'

Error types

import {
  AuthzError,      // base class
  ParseError,      // invalid .toon syntax  — has .line, .column, .sourcePath
  CompileError,    // semantic error        — has .sourcePath
  EvaluationError, // condition eval error
  PathSafetyError, // path traversal attempt
  ContextError,    // prototype pollution or bad input shape
} from 'advance-authz'

Audit logging

const authz = new Authz({
  audit: (record) => {
    // AuditRecord shape:
    // {
    //   allowed: boolean
    //   userId: string | number
    //   roles: readonly string[]
    //   action: string
    //   resource: string
    //   reason: DecisionReason
    //   durationMs: number
    //   timestamp: number   // unix ms
    // }
    logger.info({ event: 'authz_decision', ...record })
  },
})

Errors thrown inside the audit callback are silently swallowed and will never propagate to the caller.


Security model

| Property | Behaviour | |---|---| | Deny by default | No rule match → access denied | | Deny overrides allow | Explicit effect: deny always wins | | No eval() | Conditions evaluated by AST tree-walk | | Input isolation | user/resource deep-cloned and frozen before evaluation | | Prototype pollution | __proto__, constructor, prototype keys rejected | | Path safety | load() and include paths both validated against process.cwd() | | Depth limit | Nested objects truncated at maxContextDepth (default: 10) |

See SECURITY.md for the vulnerability disclosure process.


Migration from v1

See MIGRATION_v1_v2.md for a complete upgrade guide.

The most important change: condition variables now use resource.x instead of the resource type name (e.g. listing.x).

- condition listing.owner_id == user.id
+ condition resource.owner_id == user.id

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Ensure pnpm ci passes (typecheck + lint + tests + build)
  4. Open a pull request

See SECURITY.md before reporting vulnerabilities.


License

Apache 2.0 — © 2025 Dhruvil