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

@pcg/auth

v1.0.0

Published

Authorization library

Downloads

1,221

Readme

@pcg/auth

Authorization library for checking user access rights. Supports a flexible permission system with scopes, wildcard access, and NestJS integration.

Installation

npm i @pcg/auth

Quick Start

import { isGranted, resolvePermissions } from '@pcg/auth';
import * as s from '@pcg/auth/scopes';

// Prepare user object
const user = {
  id: 'hcu:2x6g7l8n9op',
  permissions: ['js:core:episodes[org#hcorg:company1]:get'],
  resolvedPermissions: resolvePermissions(['js:core:episodes[org#hcorg:company1]:get']),
};

// Check access
isGranted(user, 'js:core:episodes:get', s.org('hcorg:company1')); // true
isGranted(user, 'js:core:episodes:get', s.org('hcorg:other'));    // false

Exports

The library provides two entry points:

| Import | Description | |--------|-------------| | @pcg/auth | Core functions: isGranted, resolvePermissions, ActionScopes, ScopesBulder, types | | @pcg/auth/scopes | Functional scope builders: org, id, user, and, anyScope, etc. |


Permission String Format

Pattern: <service>:<module>:<resource>[scopes]:<action>

| Component | Description | Examples | |-----------|-------------|----------| | service | Application identifier | js, tl, bo, * | | module | Module within the service | core, mam, * | | resource | Entity type | episodes, users, roles | | scopes | Constraints (optional) | [org], [org,published], [org+published] | | action | Operation | get, list, create, update, delete, * |

Permission String Examples

'js:core:episodes:get'                      // No scopes — full access to the action
'js:core:episodes[org]:get'                 // Restricted to organization
'js:core:episodes[org,published]:get'       // OR — needs ANY of the scopes
'js:core:episodes[org+published]:get'       // AND — needs ALL scopes
'js:core:episodes[org#hcorg:company1]:get'  // Bound to a specific organization
'js:*:*:*'                                  // Wildcard — all actions in the service
'*:*:*:*'                                   // Super admin — everything

Scope Logic

OR Logic (comma ,)

User is granted access if the action scope matches any of their scopes.

// Permission: 'js:core:episodes[org,published]:get'
// Resolved scopes: ['org', 'published']

isGranted(user, 'js:core:episodes:get', ['org']);       // true
isGranted(user, 'js:core:episodes:get', ['published']); // true
isGranted(user, 'js:core:episodes:get', ['draft']);     // false

AND Logic (plus +)

User is granted access only if the action scope contains all specified scopes.

// Permission: 'js:core:episodes[org+published]:get'
// Resolved scopes: [['org', 'published']]  ← array within array

isGranted(user, 'js:core:episodes:get', ['org']);                  // false
isGranted(user, 'js:core:episodes:get', [['org', 'published']]);   // true

Mixed Logic

// Permission: 'js:core:episodes[published,org+draft]:get'
// Resolved scopes: ['published', ['org', 'draft']]

// Access granted if:
//   - scope is 'published', OR
//   - both 'org' AND 'draft' scopes

isGranted(user, 'js:core:episodes:get', ['published']);       // true
isGranted(user, 'js:core:episodes:get', [['org', 'draft']]);  // true
isGranted(user, 'js:core:episodes:get', ['org']);              // false

Scope ID (hash #)

Scopes can include a binding to a specific entity ID.

// Permission: 'js:core:episodes[org#hcorg:company1]:get'

isGranted(user, 'js:core:episodes:get', ['org#hcorg:company1']); // true
isGranted(user, 'js:core:episodes:get', ['org#hcorg:company2']); // false

Wildcard (*)

The ['*'] action scope bypasses scope checking.

isGranted(user, 'js:core:episodes:get', ['*']); // true — if user has the permission with any scope

Results Matrix

| User Permission | Action Scopes | Result | |-----------------|---------------|--------| | episodes:get (no scopes) | any | true | | episodes[org]:get | ['org'] | true | | episodes[org]:get | ['published'] | false | | episodes[org]:get | [] (empty) | false | | episodes[org]:get | ['*'] | true | | episodes[org,published]:get | ['org'] | true | | episodes[org+published]:get | ['org'] | false | | episodes[org+published]:get | [['org', 'published']] | true | | episodes[org#hcorg:A]:get | ['org#hcorg:A'] | true | | episodes[org#hcorg:A]:get | ['org#hcorg:B'] | false | | js:*:*:* | any | true |


Types

IUser

The user object must contain resolvedPermissions:

interface IUser {
  id: string;                                         // 'hcu:2x6g7l8n9op'
  permissions: readonly string[];                     // Raw permission strings
  resolvedPermissions: ReadonlyResolvedPermission[];  // Parsed permissions
}

ResolvedPermission

interface ResolvedPermission {
  id: string;                      // Permission without scope brackets
  scopes: (string | string[])[];   // Parsed scopes
}

// Examples:
// 'js:core:episodes[org,published]:get'
//   → { id: 'js:core:episodes:get', scopes: ['org', 'published'] }
//
// 'js:core:episodes[org+published]:get'
//   → { id: 'js:core:episodes:get', scopes: [['org', 'published']] }

API

isGranted(user, permission, actionScopes?)

Main function for checking access. Returns boolean.

import { isGranted } from '@pcg/auth';
import * as s from '@pcg/auth/scopes';

// Without scopes
isGranted(user, 'js:core:episodes:get');

// With single scope (no array needed)
isGranted(user, 'js:core:episodes:get', s.org('hcorg:company1'));

// With wildcard
isGranted(user, 'js:core:episodes:list', s.anyScope());

resolvePermission(permission)

Parses a single permission string into an object.

import { resolvePermission } from '@pcg/auth';

resolvePermission('js:core:episodes[org,published]:get');
// { id: 'js:core:episodes:get', scopes: ['org', 'published'] }

resolvePermission('js:core:episodes[org+published]:get');
// { id: 'js:core:episodes:get', scopes: [['org', 'published']] }

resolvePermissions(permissions)

Parses an array of permission strings. Matching IDs are merged — their scopes are combined.

import { resolvePermissions } from '@pcg/auth';

resolvePermissions([
  'js:core:episodes[org]:get',
  'js:core:episodes[published]:get',
]);
// [{ id: 'js:core:episodes:get', scopes: ['org', 'published'] }]

mergeResolvedPermissions(array1, array2)

Merges two arrays of resolved permissions. Empty scopes ([]) mean full access and take precedence when merging.

import { mergeResolvedPermissions } from '@pcg/auth';

mergeResolvedPermissions(
  [{ id: 'js:core:episodes:get', scopes: ['org#hci'] }],
  [{ id: 'js:core:episodes:get', scopes: ['org#dv'] }],
);
// [{ id: 'js:core:episodes:get', scopes: ['org#hci', 'org#dv'] }]

// Empty scopes = full access
mergeResolvedPermissions(
  [{ id: 'js:core:episodes:get', scopes: ['org#hci'] }],
  [{ id: 'js:core:episodes:get', scopes: [] }],
);
// [{ id: 'js:core:episodes:get', scopes: [] }]

encodeScopes(scopes)

Converts a scopes array back to string format.

import { encodeScopes } from '@pcg/auth';

encodeScopes(['org#xxx', 'user#xxx']);
// '[org#xxx,user#xxx]'

encodeScopes([['org#xxx', 'published']]);
// '[org#xxx+published]'

injectScopesIntoPermission(permission, scopes)

Adds scopes to a permission string.

import { injectScopesIntoPermission } from '@pcg/auth';

injectScopesIntoPermission('js:core:episodes:create', ['org']);
// 'js:core:episodes[org]:create'

injectScopesIntoPermission('js:core:episodes[org]:create', ['shared']);
// 'js:core:episodes[org,shared]:create'

replaceScope(scopes, from, to)

Replaces specific scope values in an array.

import { replaceScope } from '@pcg/auth';

replaceScope(['assigned'], 'assigned', 'brand#brd:xxx');
// ['brand#brd:xxx']

// Works with AND scopes
replaceScope([['assigned', 'lang']], 'assigned', 'brand#brd:xxx');
// [['brand#brd:xxx', 'lang']]

// Sequential replacement
let scopes = [['assigned', 'lang']];
scopes = replaceScope(scopes, 'assigned', 'brand#brd:xxx');
scopes = replaceScope(scopes, 'lang', 'lang#en');
// [['brand#brd:xxx', 'lang#en']]

Functional Scope Builders (recommended)

Imported from @pcg/auth/scopes. Pure functions with no classes — convenient for inline use.

isGranted accepts a single scope string directly or an array for multiple scopes.

import * as s from '@pcg/auth/scopes';

Available Functions

| Function | Returns | Example | |----------|---------|---------| | s.anyScope() | ActionScopesArray | s.anyScope()['*'] | | s.org(id) | string | s.org('jsorg:hci')'org#jsorg:hci' | | s.id(entityId) | string | s.id('ep:123')'id#ep:123' | | s.user(id) | string | s.user('hcu:xxx')'user#hcu:xxx' | | s.form(id) | string | s.form('contact')'form#contact' | | s.group(id) | string | s.group('hcgrp:ZT9')'grp#hcgrp:ZT9' | | s.scope(name, id?) | string | s.scope('orggroup', 'hcgrp:ZT9')'orggroup#hcgrp:ZT9' | | s.and(...items) | string[] | s.and(s.org('hci'), 'published')['org#hci', 'published'] |

Usage Examples

import * as s from '@pcg/auth/scopes';
import { isGranted } from '@pcg/auth';

// Single scope — no array needed
isGranted(user, 'js:core:brands:update', s.org(brand.orgId));

// Multiple scopes (OR) — use array
isGranted(user, 'js:core:episodes:get', [s.org(episode.orgId), s.id(episode.id)]);

// AND scopes (reuse variable)
const org = s.org(episode.orgId);
isGranted(user, 'js:core:episodes:get', [org, s.and(org, 'published')]);

// Wildcard
isGranted(user, 'js:core:episodes:list', s.anyScope());

// Custom scope
isGranted(user, 'js:mam:episodes:create', s.scope('action', 'approve'));

ActionScopes (legacy)

The ActionScopes class is still supported for backward compatibility. Use functional scope builders for new code.

import { ActionScopes, isGranted } from '@pcg/auth';

const scopes = new ActionScopes();
scopes.set('org', 'hcorg:company1');    // org#hcorg:company1
scopes.set('org+published');            // AND: ['org#hcorg:company1', 'published']
scopes.setEntityId('jse:episode123');   // id#jse:episode123

isGranted(user, 'js:core:episodes:get', scopes);

ScopesBulder

Utility for building complex scope combinations.

import { ScopesBulder } from '@pcg/auth';

const builder = new ScopesBulder();

// Add scopes
builder.append('org#hci');
builder.extend(['org#hci', 'org#dv']);

// Join — creates AND combinations (cartesian product)
builder.extend(['published', 'draft']);
builder.join('org#hcc', 'before');
// [['org#hcc', 'published'], ['org#hcc', 'draft']]

// Replace prefix
builder.replacePrefix('org', 'id');
// ['org#hcorg:hci'] → ['id#hcorg:hci']

// Clone and build
const clone = builder.clone();
const scopes = builder.build();

Example: Multi-language, multi-org access

const builder = new ScopesBulder();
builder.extend(['org#hci', 'org#dv']);
builder.join(['lang#en', 'lang#de']);
builder.build();
// [
//   ['org#hci', 'lang#en'],
//   ['org#hci', 'lang#de'],
//   ['org#dv', 'lang#en'],
//   ['org#dv', 'lang#de'],
// ]

NestJS Integration

Basic CRUD

import * as s from '@pcg/auth/scopes';

@Resolver(() => Episode)
@UseGuards(GraphQLJwtAuthGuard, GraphQLPermissionsGuard)
export class EpisodesResolver {

  @Query(() => Episode)
  @UsePermission('js:core:episodes:get')
  async episode(
    @Args('id') id: string,
    @ActionContextParam() ctx: ActionContext,
  ) {
    const episode = await this.episodesService.findOne(id);

    ctx.validateAccess(
      'js:core:episodes:get',
      [s.org(episode.organizationId), s.id(episode.id)],
    );

    return episode;
  }
}

Two-Level Permission Check

  1. @UsePermission() — guard checks whether the user has access to the action with any scope (uses ['*'] wildcard)
  2. ctx.validateAccess() — inside the resolver, checks specific scopes for the given entity
import * as s from '@pcg/auth/scopes';

@Mutation(() => Episode)
@UsePermission('js:mam:episodes:update')  // Step 1: does the user have this permission at all?
async updateEpisode(
  @Args('input') input: UpdateEpisodeInput,
  @ActionContextParam() ctx: ActionContext,
) {
  const episode = await this.episodeService.findOne(input.id);

  // Step 2: check the specific scope
  ctx.validateAccess(
    'js:mam:episodes:update',
    s.org(episode.organizationId),
  );
  // If the episode belongs to company2 but the user only has access to company1 — throws an error

  return this.episodeService.update(input, ctx);
}

ctx.validateAccess() vs ctx.isGranted()

| Method | Behavior | When to use | |--------|----------|-------------| | ctx.validateAccess() | Throws an error if access is denied | When access is mandatory | | ctx.isGranted() | Returns boolean | When branching logic is needed |

// validateAccess — throws if access is denied
ctx.validateAccess('js:core:episodes:get', s.org(episode.orgId));

// isGranted — for conditional logic
if (ctx.isGranted('js:core:episodes:list', s.anyScope())) {
  return this.episodeService.findAll();
}

Conditional Access: Full vs Scoped

import * as s from '@pcg/auth/scopes';

@Query(() => [Episode])
@UsePermission('js:mam:episodes:list')
async episodes(@ActionContextParam() ctx: ActionContext) {
  // Admin: full access (permission without scopes)
  if (ctx.isGranted('js:mam:episodes:list', s.anyScope())) {
    return this.episodeService.findAll();
  }

  // User: organization-scoped access
  if (ctx.isGranted(
    'js:mam:episodes:list',
    s.org(ctx.user.organizationId),
  )) {
    return this.episodeService.findByOrg(ctx.user.organizationId);
  }

  return [];
}

Status-Based Access (published/draft)

import * as s from '@pcg/auth/scopes';

// Permission: 'js:mam:episodes[org+published]:get'
// Can only view published episodes within their organization

async getEpisode(id: string, ctx: ActionContext) {
  const episode = await this.repository.findOne(id);

  const org = s.org(episode.organizationId);
  const items: s.ScopeItem[] = [org];

  if (episode.status === 'published') {
    items.push('published', s.and(org, 'published'));
  } else if (episode.status === 'draft') {
    items.push('draft', s.and(org, 'draft'));
  }

  ctx.validateAccess('js:mam:episodes:get', items);

  return episode;
}

Public Resources

import * as s from '@pcg/auth/scopes';

// AUTHORIZED: 'tl:core:forms[public]:get'
// ORGANIZATION_ADMIN: 'tl:core:forms[assigned-org]:get' → resolved to org#tlorg:xxx

@Query(() => Form)
@UsePermission('tl:core:forms:get')
async form(
  @Args() input: FetchFormInput,
  @ActionContextParam() ctx: ActionContext,
) {
  const form = await this.formsService.getOneByOrFail(input, ctx);

  const items: s.ScopeItem[] = [
    s.org(form.organizationId),
    s.id(form.id),
  ];

  if (form.audienceType === FormAudienceType.PUBLIC) {
    items.push('public');
  }

  ctx.validateAccess('tl:core:forms:get', items);

  return form;
}

Preparing the User Object

Permission strings are stored in the database as an array of strings. Before checking access, they must be parsed into resolvedPermissions:

import { resolvePermissions } from '@pcg/auth';

const rawPermissions = [
  'js:core:episodes[org]:get',
  'js:core:episodes[published]:get',
  'js:core:episodes[org]:create',
  'js:mam:*[org]:*',
];

const user = {
  id: 'hcu:2x6g7l8n9op',
  permissions: rawPermissions,
  resolvedPermissions: resolvePermissions(rawPermissions),
};

// resolvedPermissions:
// [
//   { id: 'js:core:episodes:get', scopes: ['org', 'published'] },
//   { id: 'js:core:episodes:create', scopes: ['org'] },
//   { id: 'js:mam:*:*', scopes: ['org'] },
// ]

Best Practices

  1. Permission naming: lowercase with colons — js:core:episodes:get
  2. Standard actions: get, list, create, update, delete
  3. Scopes: use org for organization, id# for entity
  4. Functional builders: prefer @pcg/auth/scopes over the ActionScopes class
  5. Checks in resolvers: all scope checks should be done in the resolver, NOT in the service layer
  6. @UsePermission: for basic access control at the guard level
  7. ctx.validateAccess(): for scope-specific checks inside the resolver
  8. Do not use assigned-org, assigned-form, assigned-group in action scopes — these are meant for role definitions and are automatically resolved to org#xxx / id#xxx