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

@yaos-git/access-lib

v226.0.1

Published

Pure ABAC guard composition library — composable, type-safe, zero dependencies

Downloads

68

Readme

Node Version TypeScript Version Standard Schema

Uses Vitest Uses Biome


Table of Contents

Getting Started

API Reference

Development


Overview

access-lib is a pure Attribute-Based Access Control (ABAC) library for TypeScript. Guards are composable boolean units. You define conditions, compose them with boolean algebra, and wrap functions or check access points.

No roles table. No action registry. No policy DSL. Just conditions and composition.

Standard Schema integration means any schema library with a conforming adapter works as the validator — Zod, Valibot, ArkType, and others.

What Makes This Unique

  • Guards as boolean algebra — compose with .and(), .or(), .not() to build any access expression
  • Type-safe composition — Standard Schema validates context and resource before any condition runs
  • Two modes — curried Guard (pass context explicitly) or bound BoundGuard (context resolved from a provider)
  • Immutable — every composition method returns a new guard, the original is never mutated
  • Zero runtime dependencies — Standard Schema is a type-only peer dependency; no runtime overhead

Installation

npm install @yaos-git/access-lib

Standard Schema must be available as a peer dependency. It is installed automatically in most setups.

npm install @standard-schema/spec

Install your preferred schema library and use it with createGuard:

# Zod (example)
npm install zod

Quick Start

import { createGuard, AccessDeniedError } from '@yaos-git/access-lib';
import { z } from 'zod';

// 1. Define schemas
const contextSchema = z.object({
	userId: z.string(),
	role:   z.enum(['admin', 'editor', 'viewer']),
});

const resourceSchema = z.object({
	id:      z.string(),
	ownerId: z.string(),
	status:  z.enum(['draft', 'published']),
});

// 2. Create a factory
const factory = createGuard({ contextSchema, resourceSchema });

// 3. Define guards
const isAdmin = factory(({ context }) => context.role === 'admin');
const isOwner = factory(({ context, resource }) => context.userId === resource.ownerId);
const isDraft = factory(({ resource }) => resource.status === 'draft');

// 4. Compose guards
const canPublish = isOwner.and(isDraft).or(isAdmin);

// 5. Check access
const doc    = { id: 'doc-1', ownerId: 'user-1', status: 'draft' as const };
const author = { userId: 'user-1', role: 'viewer' as const };

const allowed = await canPublish.can(author, doc);   // true

// 6. Protect a function
async function publishDocument(doc: typeof resourceSchema._type) {
	return `published ${doc.id}`;
}

const protectedPublish = canPublish.protect(publishDocument);

try {
	const result = await protectedPublish(author)(doc);
} catch (err) {
	if (err instanceof AccessDeniedError) {
		console.error('Access denied:', err.context, err.resource);
	}
}

API Reference

createGuard

Creates a guard factory bound to a context schema and a resource schema.

function createGuard<TContext, TResource>(
	config: GuardFactoryConfig<TContext, TResource>,
): GuardFactoryResult<TContext, TResource>;

function createGuard<TContext, TResource>(
	config: GuardFactoryConfigWithProvider<TContext, TResource>,
): BoundGuardFactoryResult<TContext, TResource>;

| Config field | Type | Required | Description | |---|---|:---:|---| | contextSchema | StandardSchemaV1<TContext> | Yes | Schema to validate the access context | | resourceSchema | StandardSchemaV1<TResource> | Yes | Schema to validate the resource being accessed | | contextProvider | () => TContext \| Promise<TContext> | No | If provided, returns BoundGuard instances |

Without contextProvider — returns a factory that produces Guard instances. Call .can(context, resource) with both arguments.

const factory = createGuard({ contextSchema, resourceSchema });
const isAdmin = factory(({ context }) => context.role === 'admin');

With contextProvider — returns a factory that produces BoundGuard instances. Context is resolved automatically. Call .can(resource) with only the resource.

const factory = createGuard({
	contextSchema,
	resourceSchema,
	contextProvider: () => getCurrentSession().user,
});
const isAdmin = factory(({ context }) => context.role === 'admin');

factory(condition)

Creates a guard from a condition function.

type Condition<TContext, TResource> = (params: {
	context:  TContext;
	resource: TResource;
}) => boolean | Promise<boolean>;

factory(condition: Condition<TContext, TResource>): Guard<TContext, TResource>
// or BoundGuard if factory was created with contextProvider

The condition receives validated context and resource — schemas are applied before the condition runs.


factory.extend(baseCondition)

Creates a new factory that prepends a base condition to every guard it produces. Conditions are evaluated in sequence with short-circuit semantics: if the base condition returns false, the guard's own condition is never evaluated.

factory.extend(baseCondition: Condition<TContext, TResource>): typeof factory
// All guards from adminFactory will first require role === 'admin'
const adminFactory = factory.extend(({ context }) => context.role === 'admin');

const canDeleteOwn = adminFactory(
	({ context, resource }) => context.userId === resource.ownerId,
);

The original factory is not affected. factory.extend() returns a new independent factory.


Guard

Returned by createGuard factories without a contextProvider. Both context and resource must be passed explicitly.

guard.can(context, resource)

Validates inputs and evaluates the condition.

guard.can(context: TContext, resource: TResource): Promise<boolean>

Throws ValidationError if either input fails schema validation.

guard.protect(fn)

Wraps an async function. Returns a curried function: call with context first, then with the original function's arguments.

guard.protect(fn: (...args: TArgs) => Promise<TReturn>): (context: TContext) => (...args: TArgs) => Promise<TReturn>
const protectedFn = isAdmin.protect(deleteDocument);

// Pre-bind context and reuse
const asAdmin = protectedFn(adminCtx);
await asAdmin(doc1);
await asAdmin(doc2);

Throws AccessDeniedError when the guard denies access.


BoundGuard

Returned by createGuard factories with a contextProvider. Context is resolved from the provider on every call.

boundGuard.can(resource)

Resolves context from the provider, validates both inputs, and evaluates the condition.

boundGuard.can(resource: TResource): Promise<boolean>

boundGuard.protect(fn)

Wraps an async function. Returns a flat function — context is resolved automatically from the provider.

boundGuard.protect(fn: (...args: TArgs) => Promise<TReturn>): (...args: TArgs) => Promise<TReturn>
const protectedFn = boundIsAdmin.protect(deleteDocument);
await protectedFn(doc);   // context resolved from provider

Composition

All composition methods are available on both Guard and BoundGuard. Each method accepts either a guard instance or a raw Condition function.

.and(other)

Returns a new guard that passes when both sides pass. Short-circuits on the first false.

guard.and(other: Condition<TContext, TResource> | Guard<TContext, TResource>): Guard<TContext, TResource>

.or(other)

Returns a new guard that passes when either side passes. Short-circuits on the first true.

guard.or(other: Condition<TContext, TResource> | Guard<TContext, TResource>): Guard<TContext, TResource>

.not()

Returns a new guard that inverts the result of the original.

guard.not(): Guard<TContext, TResource>

Composition examples:

// Owner AND draft
const canEditDraft = isOwner.and(isDraft);

// (Owner AND draft) OR admin
const canPublish = isOwner.and(isDraft).or(isAdmin);

// Not published
const notPublished = isPublished.not();

// Inline condition
const canEditLong = isOwner.and(({ resource }) => resource.id.length > 0);

| Method | Semantics | Short-circuit | |--------|-----------|:---:| | .and(other) | Both must pass | Yes — stops at first false | | .or(other) | Either must pass | Yes — stops at first true | | .not() | Inverts the result | N/A |


Errors

AccessDeniedError

Thrown by .protect() when the guard denies access. Extends Error.

class AccessDeniedError extends Error {
	readonly context:  unknown;   // validated context value
	readonly resource: unknown;   // validated resource value
}
try {
	await protectedFn(viewer)(doc);
} catch (err) {
	if (err instanceof AccessDeniedError) {
		// err.context  — who was denied
		// err.resource — what they were denied access to
	}
}

ValidationError

Thrown by .can() and .protect() when schema validation fails. Extends Error.

class ValidationError extends Error {
	readonly issues: readonly StandardSchemaV1.Issue[];
}

Validation is applied to both context and resource before the condition runs. If validation fails, the condition is never evaluated.


Available Scripts

Development Scripts

| Script | Description | |--------|-------------| | npm run dev | Run TypeScript checking and test watcher concurrently | | npm run dev:typescript | Run TypeScript type checking in watch mode | | npm run dev:test | Run Vitest in watch mode |

Build Scripts

| Script | Description | |--------|-------------| | npm run build | Bundle with esbuild and emit declarations |

Lint Scripts

| Script | Description | |--------|-------------| | npm run lint | Run type checking, linting, formatting, and audit | | npm run lint:types | Run TypeScript type checking only | | npm run lint:fix | Check and fix linting issues with Biome | | npm run lint:format | Format all files with Biome | | npm run lint:check | Check code for linting issues with Biome | | npm run lint:audit | Run npm audit |

Testing Scripts

| Script | Description | |--------|-------------| | npm test | Run all tests (unit, types, e2e) | | npm run test:unit | Run unit tests | | npm run test:types | Run type-level tests | | npm run test:e2e | Run end-to-end tests | | npm run coverage | Run tests with coverage report |


Tech Stack

Core

  • Standard Schema — Type-only peer dep; any conforming schema library works at runtime
  • Zod — Used in dev and tests only; not a runtime dependency

Build and Development

  • esbuild — Fast bundler
  • tsgo — TypeScript type checker (native preview)
  • Vitest — Unit and type-level testing framework
  • Biome — Linter and formatter

Project Structure

access-lib/
├── src/
│   ├── core/
│   │   ├── BoundGuard/             # BoundGuard class (contextProvider mode)
│   │   │   ├── index.ts
│   │   │   └── index.test.ts
│   │   ├── Condition/              # Boolean evaluation helpers (and/or/not)
│   │   │   ├── index.ts
│   │   │   └── index.test.ts
│   │   ├── Guard/                  # Guard class (explicit context mode)
│   │   │   ├── index.ts
│   │   │   └── index.test.ts
│   │   ├── GuardFactory/           # createGuard factory function
│   │   │   ├── index.ts
│   │   │   └── index.test.ts
│   │   └── Validator/              # Standard Schema validation adapter
│   │       ├── index.ts
│   │       └── index.test.ts
│   ├── types/
│   │   ├── Error/                  # AccessDeniedError, ValidationError
│   │   │   └── index.ts
│   │   └── Guard/                  # Condition, GuardFactoryConfig types
│   │       ├── index.ts
│   │       └── index.test-d.ts
│   └── index.ts                    # Public API surface
├── examples/
│   ├── basic-guards/               # createGuard and .can() walkthrough
│   ├── guard-composition/          # .and(), .or(), .not() walkthrough
│   └── protect-wrapper/            # .protect() and factory.extend() walkthrough
├── biome.json                      # Biome configuration
├── tsconfig.json                   # TypeScript base configuration
├── tsconfig.app.json               # App TypeScript configuration
├── tsconfig.vitest.json            # Vitest TypeScript configuration
├── vitest.config.ts                # Vitest shared configuration
├── vitest.unit.config.ts           # Unit test configuration
├── vitest.type.config.ts           # Type test configuration
├── vitest.e2e.config.ts            # E2E test configuration
├── esbuild.config.js               # esbuild bundler configuration
└── package.json

Versioning

This project uses a custom versioning scheme: MAJORYY.MINOR.PATCH

| Part | Description | Example | |------|-------------|---------| | MAJOR | Major version number | 1 | | YY | Year (last 2 digits) | 26 for 2026 | | MINOR | Minor version | 0 | | PATCH | Patch version | 0 |

Example: 126.0.0 = Major version 1, released in 2026, minor 0, patch 0


Style Guide

Conventions for contributing to this project. All rules are enforced by code review; Biome handles formatting and lint.

Exports

  • Named exports only — no export default. Every module uses export function, export const, export class, or export type.
  • import type — always use import type for type-only imports.
  • .js extensions — all relative imports use explicit .js extensions (ESM requirement).

Naming

  • PascalCase for class and type names.
  • camelCase for functions, variables, and instances.
  • PascalCase directories for core/ and types/ modules.

Formatting

  • Tab indentation — enforced by Biome.
  • Single quotes for string literals — enforced by Biome.

Types

  • Use type for all type definitions — never interface.
  • Shared types live in src/types/TypeName/index.ts with a co-located TypeName.test-d.ts.
  • No duplicate type definitions — import from the canonical source.

Testing

  • Every module has a co-located test file.
  • Type-level tests use index.test-d.ts with expectTypeOf / assertType.
  • Unit tests use index.test.ts with Vitest.

License

ISC