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

comply

v0.4.0

Published

Comply is a tiny library to help you define policies in your app

Downloads

21

Readme

Comply

GitHub Repo stars npm GitHub npm npm GitHub top language

This library provides a simple way to define and enforce policies within your application. Policies are defined as a set of rules that determine whether a specific action can be performed based on a given context.

The API surface is small:

  • definePolicy: Define a policy, the core primitive
  • definePolicies: Define a policy set (a collection of policies created with definePolicy)
  • check: Test a policy condition
  • assert: Assert a policy condition (throws if condition is not met)

TLDR

Define policies

// somewhere in your app
type MyContext = { userId: string; rolesByOrg: Record<string, "user" | "admin" | "superadmin"> };
// define all your policies
import { assert, check, definePolicies, definePolicy, matchSchema, notNull, or } from "comply";
import { z } from "zod";

const OrgPolicies = definePolicies((context: MyContext) => (orgId: string) => {
  const currentUserOrgRole = context.rolesByOrg[orgId];

  return [
    definePolicy("can administrate", or(currentUserOrgRole === "admin", currentUserOrgRole === "superadmin")),
    definePolicy("is superadmin", currentUserOrgRole === "superadmin"),
  ];
});

const UserPolicies = definePolicies((context: MyContext) => (userId: string) => [
  definePolicy("can edit profile", context.userId === userId),
]);

// create and export a 'guard' that contains all your policies, scoped by domain
export const Guard = (context: MyContext) => ({
  org: OrgPolicies(context),
  user: UserPolicies(context),
});

Use policies

// use - example with Remix Run
import { matchSchema, notNull, definePolicy, definePolicies, check, assert } from 'comply';

// route: /orgs/:orgId
const ParamsSchema = z.object({ orgId: z.string() });

export function loader({ request, context, params }: LoaderFunctionArgs) {
  const guard = Guard(context);

  // define an implicit policy on the fly!
  assert("params are valid", matchSchema(ParamsSchema), params);
    // params is now typed as { orgId: string }


  //                     👇 type-safe               👇 type-safe
  if (check(guard.org(params.orgId).policy("can administrate"))) {
    console.log("User can administrate the IT department.");
  } else {
    console.log("User cannot administrate the IT department.");
  }

  assert(guard.org(params.orgId).policy("can administrate"));
  // context.rolesByOrg[params.orgId] === "admin"
  // otherwise it throws an error
}

Type-safe all the way

Accessing policies by name from policy sets is type-safe.

For example, with guard.org(params.orgId).policy("can administrate"), "can administrate" will be suggested by Typescript.

If the condition requires a parameter, assert and check will require it.

Finally, if the condition is a type guard, the parameter you pass will be inferred automatically.

Defining Policies

To define policies, you create a policy set using the definePolicies function. Each policy definition is created using the definePolicy function, which takes a policy name and a callback that defines the policy logic (or a boolean value).

[!IMPORTANT] For convenience, the condition can be a boolean value but you will lose type inference

If you want TS to infer something (not null, union, etc), use a condition function

The callback logic can receive a unique parameter (scalar or object) and return a boolean value or a a type predicate.

You can also provide an error factory to the policy (3rd argument) to customize the error message.

definePolicies returns a policy set (a collection of policies you can invoke with .policy("name")) or a policy set factory (a function that takes a parameter and returns a policy set).

You can then use this set to check if a condition is met and/or assert it with check and assert.

Simple policy set

definePolicies accepts an array of policies created with definePolicy.

Primary use case: simple policies that can be defined inline and are 'self-contained' (don't need a context or a factory).

const policies = definePolicies([
  // built-in type guards
  definePolicy("is not null", notNull),
  definePolicy('params are valid', matchSchema(z.object({ name: z.string() }))),
  // type guard
  definePolicy("is a string", (v: unknown): v is string => typeof v === "string"),
]);

Advanced policy set

definePolicies can take a callback that receives a context (whatever you want to pass to your policies) and returns a policy set or a policy set factory.

A policy set factory is a function that takes a parameter (scalar or object) and returns a policy set.

The primary purpose of this is to simplify the definition of policies that depend on a parameter (e.g. a userId, orgId, etc.).

Here's a quick example:

// 1️⃣
type Context = { userId: string; rolesByOrg: Record<string, "user" | "admin" | "superadmin">; appRole: "admin" | "user" };

const AdminPolicies = definePolicies((context: Context) => [
  definePolicy("has app admin role", context.appRole === "admin")
]);

// 2️⃣
const OrgPolicies = definePolicies((context: MyContext) => (orgId: string) => {
  const adminGuard = AdminPolicies(context);
  const currentUserOrgRole = context.rolesByOrg[orgId];

  return [
    definePolicy("can administrate", () =>
      or(
        () => currentUserOrgRole === "admin",
        () => currentUserOrgRole === "superadmin",
        () => check(adminGuard.policy("has app admin role"))
    ),
    definePolicy("is superadmin", () => currentUserOrgRole === "superadmin"),
  ];
});

// other policies...

// 3️⃣
// create and export a 'guard' that contains all your policies, scoped by domain
export const Guard = (context: Context) => ({
  org: OrgPolicies(context),
});

Let's break it down:

1️⃣

We define a context type that includes the necessary information for our policies.

It's up to you what you put in it, depending on what framework you're using and what information you need in your policies.

2️⃣

We create a policy set factory that takes a orgId and returns a policy set.

This way, we can 'scope' our policies to a specific organization and benefit from the closure feature (all policies share the same currentUserOrgRole variable).

We also can invoke other policy sets factories (e.g. AdminPolicies) and compose new policies.

3️⃣

We create and export a Guard function (arbitrary name) that takes a context and returns an object containing all our policies.

We choose to scope our policies by domain (e.g. org, user, params, etc.) to avoid conflicts and make the code more organized.

Using Policies

To use your policies, invoke the Guard factory with the context and then use the returned object to access your policies.

Here's an example with a Remix Run loader but it works the same with any other framework.

import { matchSchema, notNull, definePolicy, definePolicies, check, assert } from 'comply';

// route: /orgs/:orgId
const ParamsSchema = z.object({ orgId: z.string() });

export function loader({ request, context, params }: LoaderFunctionArgs) {
  const guard = Guard(context);

  // 1️⃣ define an implicit policy on the fly!
  assert("params are valid", matchSchema(ParamsSchema), params)
  // params is now typed as { orgId: string }

  // 2️⃣                     👇 type-safe               👇 type-safe
  if (check(guard.org(params.orgId).policy("can administrate"))) {
    console.log("User can administrate the IT department.");
  } else {
    console.log("User cannot administrate the IT department.");
  }

  // 3️⃣
  assert(guard.org(params.orgId).policy("can administrate"))
  // context.rolesByOrg[params.orgId] === "admin"
  // otherwise it throws an error
}

Let's break it down:

1️⃣

Just to demonstrate that we can, we define an implicit policy on the fly!

It's a quick way to name an assert/check in your code flow.

It works the same for check and it's equivalent to defining a policy with definePolicy.

2️⃣

We use check and pass it the policy we want to evaluate. We are telling a story here: "check if the user can administrate this specific organization".

3️⃣

We use assert to assert a policy condition. It passes or it throws an error.

Async Policy Evaluation

The library does not support async policy evaluation because TypeScript does not support async type guards. (https://github.com/microsoft/TypeScript/issues/37681).

Of course, we can use async check but not directly in policy conditions.

Here's an example:

type Context = { userId: string; rolesByOrg: Record<string, "user" | "admin"> };

const OrgPolicies = definePolicies((context: Context) => (orgId: string) => [
  definePolicy("can administrate org", (stillOrgAdmin: boolean) =>
    and(context.rolesByOrg[orgId] === "admin", stillOrgAdmin)
  ),
]);

// fake server check
async function checkIfStillOrgAdmin(orgId: string, userId: string) {
  // ...
}

  // route: /orgs/:orgId
const ParamsSchema = z.object({ orgId: z.string() });

export async function loader({ request, context, params }: LoaderFunctionArgs) {
  const guard = Guard(context);

  assert("params are valid", matchSchema(ParamsSchema), params)

  assert(guard.org(params.orgId).policy("can administrate org"), await checkIfStillOrgAdmin(params.orgId, context.userId))
}

In this example, our policy condition requires a parameter (stillOrgAdmin, boolean, but can be any type).

Then we use inversion of control to pass the parameter to the policy condition.

This is not what I really want, but it's a temporary limitation we have to live with until TypeScript implements async type guards.

I prefer to preserve the type guard/inference benefits of assert and check instead of supporting async policy conditions.

API

definePolicy

Core primitive to define a policy.

type PolicyError = Error;
type PolicyErrorFactory<T extends PolicyError = PolicyError> = (arg: unknown) => T;
type PolicyConditionWithArg<T = any> = (arg: T) => boolean;
type PolicyConditionArg<P extends PolicyCondition> = P extends PolicyConditionWithArg<infer T> ? T : never;
type PolicyConditionTypeGuard<T = any, U extends T = T> = (arg: T) => arg is U;
type PolicyConditionTypeGuardResult<P extends PolicyCondition> = P extends PolicyConditionTypeGuard<any, infer U>
  ? U
  : PolicyConditionArg<P>;
type PolicyConditionNoArg = (() => boolean) | boolean;
type PolicyCondition<T = any, U extends T = T> =
  | PolicyConditionTypeGuard<T, U>
  | PolicyConditionWithArg<T>
  | PolicyConditionNoArg;

function definePolicy(name: string, condition: PolicyCondition, errorFactory?: PolicyErrorFactory)

Example:

const postHasCommentsPolicy = definePolicy(
  "post has comments",
  (post: Post) => post.comments.length > 0,
  () => new Error("Post has no comments")
);

definePolicies

Core primitive to define a policy set (collection of policies).

function definePolicies<T extends AnyPolicies>(policies: T): PolicySet<T>;

type AnyPolicy = Policy<PolicyName, PolicyCondition>;
type AnyPolicies = AnyPolicy[];
type PolicyFactory = (...args: any[]) => AnyPolicies;
type PoliciesOrFactory = AnyPolicies | PolicyFactory;
type PolicySetOrFactory<T extends PoliciesOrFactory> = T extends AnyPolicies
  ? PolicySet<T>
  : T extends PolicyFactory
    ? (...args: Parameters<T>) => PolicySet<ReturnType<T>>
    : never;
type WithRequiredContext<T> = T extends (arg: infer A) => any ? (unknown extends A ? never : T) : never;

function definePolicies<Context, T extends PoliciesOrFactory>(
  define: WithRequiredContext<(context: Context) => T>
): (context: Context) => PolicySetOrFactory<T>;

Example:

const PostPolicies = definePolicies((context: Context) => [
  definePolicy("post has comments", (post: Post) => post.comments.length > 0),
  definePolicy("is author", (post: Post) => post.authorId === context.userId)
]);

export const Guard = (context: Context) => ({
  post: PostPolicies(context),
});

assert

function assert(name: string, condition: PolicyConditionNoArg): void;

function assert<TPolicyCondition extends PolicyConditionTypeGuard<any> | PolicyConditionWithArg<any>>(
  name: string,
  condition: TPolicyCondition,
  arg: TPolicyCondition extends PolicyConditionNoArg ? never : PolicyConditionArg<TPolicyCondition>
): asserts arg is TPolicyCondition extends PolicyConditionNoArg
  ? never
  : PolicyConditionTypeGuardResult<TPolicyCondition>;

function assert<TPolicyCondition extends PolicyConditionNoArg>(
  policy: Policy<PolicyName, TPolicyCondition, PolicyErrorFactory>
): void;

function assert<TPolicyCondition extends PolicyConditionTypeGuard<any> | PolicyConditionWithArg<any>>(
  policy: Policy<PolicyName, TPolicyCondition, PolicyErrorFactory>,
  arg: TPolicyCondition extends PolicyConditionNoArg ? never : PolicyConditionArg<TPolicyCondition>
): asserts arg is TPolicyCondition extends PolicyConditionNoArg
  ? never
  : PolicyConditionTypeGuardResult<TPolicyCondition>;

Example:

const guard = Guard(context);

const post = await fetchPost(id);

assert(guard.post.policy("is author"), post);
// post.authorId === context.userId

check

function check(name: string, condition: PolicyConditionNoArg): boolean;

function check<TPolicyCondition extends PolicyConditionTypeGuard<any> | PolicyConditionWithArg<any>>(
  name: string,
  condition: TPolicyCondition,
  arg: TPolicyCondition extends PolicyConditionNoArg ? never : PolicyConditionArg<TPolicyCondition>
): arg is TPolicyCondition extends PolicyConditionNoArg ? never : PolicyConditionTypeGuardResult<TPolicyCondition>;

function check<TPolicyCondition extends PolicyConditionNoArg>(
  policy: Policy<PolicyName, TPolicyCondition>
): boolean;

function check<TPolicyCondition extends PolicyConditionTypeGuard<any> | PolicyConditionWithArg<any>>(
  policy: Policy<PolicyName, TPolicyCondition>,
  arg: TPolicyCondition extends PolicyConditionNoArg ? never : PolicyConditionArg<TPolicyCondition>
): arg is TPolicyCondition extends PolicyConditionNoArg ? never : PolicyConditionTypeGuardResult<TPolicyCondition>;

Example:

const guard = Guard(context);

const post = await fetchPost(id);

if (check(guard.post.policy("post has comments"), post)) {
  console.log("Post has comments");
}

checkAllSettle

Evaluates all the policies with check and returns a snapshot with the results.

It's useful to serialize policies.

It takes an array of policies. If a policy does not take an argument, it can be passed as is. Policies that take an argument have to be passed as a tuple with the argument.

type PolicyTuple =
  | Policy<string, PolicyConditionNoArg>
  | readonly [string, PolicyConditionNoArg]
  | readonly [Policy<string, PolicyConditionNoArg>]
  | readonly [Policy<string, PolicyConditionWithArg>, any];
type InferPolicyName<TPolicyTuple> = TPolicyTuple extends readonly [infer name, any]
  ? name extends Policy<infer Name, any>
    ? Name
    : name extends string
      ? name
      : never
  : TPolicyTuple extends readonly [Policy<infer Name, any>]
    ? Name
    : TPolicyTuple extends Policy<infer Name, any>
      ? Name
      : never;
type PoliciesSnapshot<TPolicyName extends string> = { [K in TPolicyName]: boolean };

export function checkAllSettle<
  const TPolicies extends readonly PolicyTuple[],
  TPolicyTuple extends TPolicies[number],
  TPolicyName extends InferPolicyName<TPolicyTuple>,
>(policies: TPolicies): PoliciesSnapshot<TPolicyName>

Example:

// TLDR
const snapshot = checkAllSettle([
  [guard.post.policy("is my post"), post], // Policy with argument
  ["post has comments", post.comments.length > 0], // Implicit policy with no argument
  definePolicy("post has likes", post.likes.length > 0), // Policy without argument
]);

// Example
const PostPolicies = definePolicies((context: Context) => {
  const myPostPolicy = definePolicy(
    "is my post",
    (post: Post) => post.userId === context.userId,
    () => new Error("Not the author")
  );

  return [
    myPostPolicy,
    definePolicy("published post or mine", (post: Post) =>
      or(check(myPostPolicy, post), post.status === "published")
    ),
  ];
});

const guard = {
  post: PostPolicies(context),
};

const snapshot = checkAllSettle([
  [guard.post.policy("is my post"), post],
  ["post has comments", post.comments.length > 0],
  definePolicy("post has likes", post.likes.length > 0),
]);

console.log(snapshot); // { "is my post": boolean; "post has comments": boolean; "post has likes": boolean }
console.log(snapshot["is my post"]) // boolean

Condition helpers

or

Logical OR operator for policy conditions.

function or(...conditions: (() => Policy<PolicyName, PolicyCondition, PolicyConditionArg<PolicyCondition>> | boolean)[])

Example:

const PostPolicies = definePolicies((context: Context) => {
  const myPostPolicy = definePolicy(
    "my post",
    (post: Post) => post.userId === context.userId,
    () => new Error("Not the author")
  );

  return [
    myPostPolicy,
    definePolicy("all published posts or mine", (post: Post) =>
      or(
        () => check(myPostPolicy, post),
        () => post.status === "published"
      )
    ),
    definePolicy("[lazy] all published posts or mine", (post: Post) =>
      or(check(myPostPolicy, post), post.status === "published")
    ),
  ];
});

and

Logical AND operator for policy conditions.

function and(
  ...conditions: (() => Policy<PolicyName, PolicyCondition, PolicyConditionArg<PolicyCondition>> | boolean)[]
)

Example:

const PostPolicies = definePolicies((context: Context) => {
  const myPostPolicy = definePolicy(
    "my post",
    (post: Post) => post.userId === context.userId,
    () => new Error("Not the author")
  );

  return [
    myPostPolicy,
    definePolicy("my published post", (post: Post) =>
      and(
        () => check(myPostPolicy, post),
        () => post.status === "published"
      )
    ),
    definePolicy("[lazy] my published post", (post: Post) =>
      and(check(myPostPolicy, post), post.status === "published")
    ),
  ];
});