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

@levante-framework/permissions-core

v1.1.2

Published

Shared permissions service for front-end and back-end

Downloads

148

Readme

Permissions Service

A TypeScript package implementing a resource-based access control system for multi-site platforms. Designed for use in both frontend (Vue SPA) and backend (Firebase Cloud Functions) environments.

Features

  • Multi-site Support: Site-scoped permissions with super admin global access
  • Role Hierarchy: Five-tier role system from participant to super admin
  • Resource-based Access Control: Granular permissions with nested sub-resources for groups and admins
  • Caching: TTL-based caching with user-specific clearing and automatic cleanup
  • Decision Logging (opt-in): Configurable modes with pluggable sinks for observability
  • Version Management: Document validation and migration framework
  • TypeScript: Full type safety with comprehensive interfaces
  • ESM: Modern ES module support with source maps

Installation

npm install permissions-service

Quick Start

Basic Usage

import { PermissionService, CacheService } from 'permissions-service';

const cache = new CacheService();

const loggingConfig = { mode: 'off' as const }; // 'off' | 'baseline' | 'debug'
const sink = {
  isEnabled: () => loggingConfig.mode !== 'off',
  emit: (event) => {
    // no-op by default; plug in Firestore, console, etc.
  }
};

const permissions = new PermissionService(cache, loggingConfig, sink);

// Check if user can perform action on a nested resource
const canEdit = permissions.canPerformSiteAction(
  user,
  'site456',
  'groups',
  'update',
  'schools' // sub-resource required for groups
);

if (canEdit) {
  // User can edit schools
}

Cloud Functions Integration

// functions/src/permissions.ts
import { onCall, HttpsError } from 'firebase-functions/v2/https';
import { getFirestore } from 'firebase-admin/firestore';
import { PermissionService, CacheService } from 'permissions-service';

// Module-level cache for container persistence
const cache = new CacheService();

const loggingConfig = {
  mode: process.env.PERM_LOG_MODE ?? 'baseline'
};

const firestoreSink = {
  isEnabled: () => loggingConfig.mode !== 'off',
  emit: (event) => {
    setImmediate(async () => {
      await getFirestore()
        .collection('permission_events')
        .add({ ...event, expireAt: Date.now() + 1000 * 60 * 60 * 24 * 90 }); // 90-day TTL
    });
  }
};

export const updateGroup = onCall(async (request) => {
  const permissions = new PermissionService(cache, loggingConfig, firestoreSink);
  const { userId, siteId } = request.auth;
  
  // Check permission
  const canUpdate = await permissions.hasPermission(
    userId,
    siteId,
    'groups',
    'update'
  );
  
  if (!canUpdate) {
    throw new HttpsError('permission-denied', 'Insufficient permissions');
  }
  
  // Proceed with update
});

Vue SPA Integration

// composables/usePermissions.ts
import { PermissionService, CacheService } from 'permissions-service';
import { ref, computed } from 'vue';

// Session-level cache
const cache = new CacheService();
const permissions = new PermissionService(cache);

export function usePermissions() {
  const currentUser = ref(null);
  const currentSite = ref(null);
  
  const canCreateGroups = computed(async () => {
    if (!currentUser.value || !currentSite.value) return false;
    
    return await permissions.hasPermission(
      currentUser.value.id,
      currentSite.value.id,
      'groups',
      'create'
    );
  });
  
  return {
    canCreateGroups,
    hasPermission: permissions.hasPermission.bind(permissions)
  };
}

Logging & Observability

Permission decisions remain boolean for callers, but you can enable structured logging by supplying a LoggingModeConfig and sink:

import { PermissionService, CacheService } from 'permissions-service';

const cache = new CacheService();
const loggingConfig = { mode: 'baseline' as const };

const sink = {
  isEnabled: () => loggingConfig.mode !== 'off',
  emit: (event) => {
    // Persist to Firestore, enqueue to Pub/Sub, etc.
    // Keep payloads de-identified (avoid IP / user agent).
  }
};

const permissions = new PermissionService(cache, loggingConfig, sink);

Recommended sink patterns:

  • Firestore (backend) — write each event with a TTL:

    const FirestoreSink = {
      isEnabled: () => true,
      emit: (event) => {
        setImmediate(async () => {
          await db.collection('permission_events').add({
            ...event,
            expireAt: Date.now() + 1000 * 60 * 60 * 24 * 60 // 60 days
          });
        });
      }
    };
  • Beacon (frontend) — forward sampled events to an HTTPS endpoint:

    const BrowserSink = {
      isEnabled: () => true,
      emit: (event) => {
        const { userId, ...sanitized } = event; // strip identifiers if required
        navigator.sendBeacon('/api/permission-log', JSON.stringify(sanitized));
      }
    };

Toggle logging modes via environment variables or Remote Config ('off' → no emission, 'baseline' → minimal denies, 'debug' → full capture for investigations). Return to 'off' once debugging is complete to avoid unnecessary overhead.

Role Hierarchy

The system implements a five-tier role hierarchy:

  1. participant - No admin dashboard access
  2. research_assistant - Read access + user creation
  3. admin - Subset of actions within their site
  4. site_admin - Full control over their site's resources
  5. super_admin - Full system access across all sites

Nested Permissions Structure

The permission system uses nested sub-resources for groups and admins:

Group Sub-Resources:

  • sites - Site-level groups
  • schools - School-level groups
  • classes - Class-level groups
  • cohorts - Cohort-level groups

Admin Sub-Resources:

  • site_admin - Site administrator accounts
  • admin - Admin accounts
  • research_assistant - Research assistant accounts

Flat Resources:

  • assignments - Task assignments
  • users - User accounts
  • tasks - System tasks

Permission Matrix Example

{
  "admin": {
    "groups": {
      "sites": ["read", "update"],
      "schools": ["read", "update", "delete"],
      "classes": ["read", "update", "delete"],
      "cohorts": ["read", "update", "delete"]
    },
    "admins": {
      "site_admin": ["read"],
      "admin": ["read"],
      "research_assistant": ["create", "read"]
    },
    "assignments": ["create", "read", "update", "delete"],
    "users": ["create", "read", "update"],
    "tasks": ["read"]
  }
}

API Reference

PermissionService

Constructor

new PermissionService(
  cache?: CacheService,
  loggingConfig?: LoggingModeConfig,
  sink?: PermEventSink
)
  • loggingConfig defaults to { mode: 'off' }.
  • sink defaults to the internal no-op sink; callers can supply Firestore/beacon/etc.

Methods

canPerformSiteAction(user, siteId, resource, action, subResource?)

Check if a user has permission to perform an action on a resource within a site.

// Nested resource (requires sub-resource)
const canEditSchools = permissions.canPerformSiteAction(
  user,
  'site456', 
  'groups',
  'update',
  'schools' // required for nested resources
);

// Flat resource (no sub-resource needed)
const canEditUsers = permissions.canPerformSiteAction(
  user,
  'site456',
  'users',
  'update'
);
canPerformGlobalAction(user, resource, action, subResource?)

Check if a super admin can perform a global action.

const canManageAdmins = permissions.canPerformGlobalAction(
  superAdminUser,
  'admins',
  'delete',
  'admin'
);
bulkPermissionCheck(user, siteId, checks)

Bulk permission checking for multiple resource/action combinations.

const results = permissions.bulkPermissionCheck(user, 'site456', [
  { resource: 'groups', action: 'create', subResource: 'schools' },
  { resource: 'users', action: 'read' }
]);
// Returns: [{ resource: 'groups', action: 'create', subResource: 'schools', allowed: true }, ...]
getAccessibleResources(user, siteId, action)

Get flat resources the user can perform an action on.

const resources = permissions.getAccessibleResources(user, 'site456', 'create');
// Returns: ['assignments', 'users'] (only flat resources)
getAccessibleGroupSubResources(user, siteId, action)

Get group sub-resources the user can perform an action on.

const groupTypes = permissions.getAccessibleGroupSubResources(user, 'site456', 'create');
// Returns: ['schools', 'classes', 'cohorts']
getAccessibleAdminSubResources(user, siteId, action)

Get admin sub-resources the user can perform an action on.

const adminTypes = permissions.getAccessibleAdminSubResources(user, 'site456', 'create');
// Returns: ['research_assistant']
getUserRole(userId, siteId)

Get the user's role for a specific site.

const role = await permissions.getUserRole('user123', 'site456');
// Returns: 'admin' | 'site_admin' | etc.
clearUserCache(userId)

Clear cached data for a specific user.

await permissions.clearUserCache('user123');

CacheService

Constructor

new CacheService(defaultTtl?: number) // Default: 5 minutes

Methods

get(key)

Retrieve cached value.

const value = cache.get('user:123:permissions');
set(key, value, ttl?)

Store value in cache with optional TTL.

cache.set('user:123:permissions', permissions, 300000); // 5 minutes
delete(key) / clear()

Remove specific key or clear entire cache.

cache.delete('user:123:permissions');
cache.clear();

User Data Structure

Users must have the following structure in Firestore:

interface User {
  id: string;
  roles: Array<{
    siteId: string;
    role: 'participant' | 'research_assistant' | 'admin' | 'site_admin' | 'super_admin';
  }>;
  userType?: 'admin' | 'student' | 'teacher' | 'caregiver';
}

Permission Matrix Document

The system expects a permission matrix document with nested structure:

interface PermissionMatrix {
  [role: string]: {
    groups: {
      sites: Action[];
      schools: Action[];
      classes: Action[];
      cohorts: Action[];
    };
    admins: {
      site_admin: Action[];
      admin: Action[];
      research_assistant: Action[];
    };
    assignments: Action[];
    users: Action[];
    tasks: Action[];
  };
}

interface PermissionDocument {
  permissions: PermissionMatrix;
  version: string;
  updatedAt: string;
}

Example Document (stored at system/permissions):

{
  "permissions": {
    "site_admin": {
      "groups": {
        "sites": ["read", "update"],
        "schools": ["create", "read", "update", "delete", "exclude"],
        "classes": ["create", "read", "update", "delete", "exclude"],
        "cohorts": ["create", "read", "update", "delete", "exclude"]
      },
      "assignments": ["create", "read", "update", "delete", "exclude"],
      "users": ["create", "read", "update", "delete", "exclude"],
      "admins": {
        "site_admin": ["create", "read"],
        "admin": ["create", "read", "update", "delete", "exclude"],
        "research_assistant": ["create", "read", "update", "delete"]
      },
      "tasks": ["create", "read", "update", "delete", "exclude"]
    }
  },
  "version": "1.1.0",
  "updatedAt": "2025-09-29T00:00:00Z"
}

Error Handling

The service throws specific errors for different scenarios:

try {
  const canEdit = await permissions.hasPermission(userId, siteId, 'groups', 'update');
} catch (error) {
  if (error.message.includes('User not found')) {
    // Handle missing user
  } else if (error.message.includes('Permission matrix not found')) {
    // Handle missing configuration
  }
}

Performance Considerations

Caching Strategy

  • Frontend: Session-level cache, cleared on user/site changes
  • Backend: Module-level cache for container persistence
  • TTL: Default 5 minutes, configurable per cache instance
  • Bulk Operations: Use hasPermissions() for multiple checks

Best Practices

  1. Reuse Cache Instances: Create once per session/container
  2. Bulk Checks: Use hasPermissions() for multiple permission checks
  3. Clear Cache: Clear user cache after role changes
  4. Error Handling: Always handle permission check failures gracefully

Development

Build

npm run build    # Compile TypeScript
npm run dev      # Watch mode
npm run clean    # Remove dist directory

Testing

npm test         # Run tests in watch mode
npm run test:run # Run tests once

Package Testing

npm pack         # Create tarball for local testing

Migration from Organization-based Permissions

This package replaces organization-based permissions with resource-based permissions. Key changes:

  • Roles are now site-scoped instead of organization-scoped
  • Permissions are defined per resource/action combination
  • Super admin role provides global access across all sites
  • No permission management UI (roles are backend-managed)

License

TBD