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

@feedmepos/hrm-permission

v1.0.9

Published

Permission types, enums, and access checking for FeedMe services.

Readme

@feedmepos/hrm-permission

Permission types, enums, and access checking for FeedMe services.

Installation

pnpm add @feedmepos/hrm-permission

Usage

Permission Enums

All subjects are in PermissionSubjectBusinessNamespace, exposed via Permission.Subject.Business.

import {
	Permission,
	PermissionAction,
	PermissionSubjectBusinessNamespace,
} from '@feedmepos/hrm-permission';

// Actions
PermissionAction.manage; // full control
PermissionAction.read;
PermissionAction.create;
PermissionAction.update;
PermissionAction.delete;

// Subjects — use either form, they are the same value
Permission.Subject.Business.hrm_teamMember; // 'business::hrm::teamMember'
PermissionSubjectBusinessNamespace.hrm_teamMember; // same

// Common subjects by module
Permission.Subject.Business.menu_item; // 'business::menu::item'
Permission.Subject.Business.menu_catalog; // 'business::menu::catalog'
Permission.Subject.Business.menu_menuManagement; // 'business::menu::menuManagement'
// ... (see full menu list in the collapsible below)
Permission.Subject.Business.restaurant; // 'business::restaurant'

Permission.Subject.Business.crm_promotion; // 'business::crm::promotion'
Permission.Subject.Business.crm_voucher; // 'business::crm::voucher'
Permission.Subject.Business.crm_membership; // 'business::crm::membership'

Permission.Subject.Business.payment_payoutAccount; // 'business::payment::payoutAccount'
Permission.Subject.Business.payment_paymentOnboarding; // 'business::payment::paymentOnboarding'
Permission.Subject.Business.payment_transactions; // 'business::payment::transactions'
Permission.Subject.Business.payment_settlements; // 'business::payment::settlements'

Permission.Subject.Business.inventory_stockBalance; // 'business::inventory::stockBalance'
Permission.Subject.Business.inventory_ingredient; // 'business::inventory::ingredient'
Permission.Subject.Business.inventory_recipe; // 'business::inventory::recipe'
// ... (see full list in the collapsible below)

Permission.Subject.Business.hrm_employee; // 'business::hrm::employee'
Permission.Subject.Business.hrm_teamMember; // 'business::hrm::teamMember'
Permission.Subject.Business.hrm_auditLog; // 'business::hrm::auditLog'

Permission.Subject.Business.report_createReport; // 'business::report::createReport'
Permission.Subject.Business.report_accessOverview; // 'business::report::accessOverview'
// General
Permission.Subject.Business.profile;
Permission.Subject.Business.restaurant;

// Menu
Permission.Subject.Business.menu_item; // 'business::menu::item'
Permission.Subject.Business.menu_catalog; // 'business::menu::catalog'
Permission.Subject.Business.menu_category; // 'business::menu::category'
Permission.Subject.Business.menu_subCategory; // 'business::menu::subCategory'
Permission.Subject.Business.menu_group; // 'business::menu::group'
Permission.Subject.Business.menu_takeaway; // 'business::menu::takeaway'
Permission.Subject.Business.menu_scheduler; // 'business::menu::scheduler'
Permission.Subject.Business.menu_variant; // 'business::menu::variant'
Permission.Subject.Business.menu_cookingGuide; // 'business::menu::cookingGuide'
Permission.Subject.Business.menu_printRoute; // 'business::menu::printRoute'
Permission.Subject.Business.menu_servingSequence; // 'business::menu::servingSequence'
Permission.Subject.Business.menu_unit; // 'business::menu::unit'
Permission.Subject.Business.menu_ingredient; // 'business::menu::ingredient'
Permission.Subject.Business.menu_recipe; // 'business::menu::recipe'
Permission.Subject.Business.menu_settings; // 'business::menu::settings'
Permission.Subject.Business.menu_publish; // 'business::menu::publish'
Permission.Subject.Business.menu_menuManagement; // 'business::menu::menuManagement'
Permission.Subject.Business.menu_importExport; // 'business::menu::importExport'

// CRM
Permission.Subject.Business.crm_promotion; // 'business::crm::promotion'
Permission.Subject.Business.crm_voucher; // 'business::crm::voucher'
Permission.Subject.Business.crm_membership; // 'business::crm::membership'
Permission.Subject.Business.crm_analytic; // 'business::crm::analytic'
Permission.Subject.Business.crm_tier; // 'business::crm::tier'
Permission.Subject.Business.crm_title; // 'business::crm::title'
Permission.Subject.Business.crm_broadcast; // 'business::crm::broadcast'
Permission.Subject.Business.crm_point; // 'business::crm::point'
Permission.Subject.Business.crm_credit; // 'business::crm::credit'
Permission.Subject.Business.crm_experience; // 'business::crm::experience'
Permission.Subject.Business.crm_game; // 'business::crm::game'
Permission.Subject.Business.crm_mission; // 'business::crm::mission'
Permission.Subject.Business.crm_loyaltyMember; // 'business::crm::loyaltyMember'
Permission.Subject.Business.crm_loyaltySegment; // 'business::crm::loyaltySegment'
Permission.Subject.Business.crm_loyaltyCard; // 'business::crm::loyaltyCard'
Permission.Subject.Business.crm_referral; // 'business::crm::referral'
Permission.Subject.Business.crm_store; // 'business::crm::store'
Permission.Subject.Business.crm_transaction; // 'business::crm::transaction'
Permission.Subject.Business.crm_setting; // 'business::crm::setting'
Permission.Subject.Business.crm_bin; // 'business::crm::bin'

// Payment
Permission.Subject.Business.payment_payoutAccount; // 'business::payment::payoutAccount'
Permission.Subject.Business.payment_paymentOnboarding; // 'business::payment::paymentOnboarding'
Permission.Subject.Business.payment_transactions; // 'business::payment::transactions'
Permission.Subject.Business.payment_settlements; // 'business::payment::settlements'

// Inventory
Permission.Subject.Business.inventory_stock; // 'business::inventory::stock'
Permission.Subject.Business.inventory_stockBalance; // 'business::inventory::stockBalance'
Permission.Subject.Business.inventory_stockAdjustment; // 'business::inventory::stockAdjustment'
Permission.Subject.Business.inventory_unitCostHistory; // 'business::inventory::unitCostHistory'
Permission.Subject.Business.inventory_wastageTemplate; // 'business::inventory::wastageTemplate'
Permission.Subject.Business.inventory_closingHistory; // 'business::inventory::closingHistory'
Permission.Subject.Business.inventory_closingTemplate; // 'business::inventory::closingTemplate'
Permission.Subject.Business.inventory_closingDraft; // 'business::inventory::closingDraft'
Permission.Subject.Business.inventory_ingredient; // 'business::inventory::ingredient'
Permission.Subject.Business.inventory_ingredientGroup; // 'business::inventory::ingredientGroup'
Permission.Subject.Business.inventory_recipe; // 'business::inventory::recipe'
Permission.Subject.Business.inventory_unit; // 'business::inventory::unit'
Permission.Subject.Business.inventory_purchaseTransfer; // 'business::inventory::purchaseTransfer'
Permission.Subject.Business.inventory_orderDraftApproval; // 'business::inventory::orderDraftApproval'
Permission.Subject.Business.inventory_transferOut; // 'business::inventory::transferOut'
Permission.Subject.Business.inventory_surcharge; // 'business::inventory::surcharge'
Permission.Subject.Business.inventory_orderTemplate; // 'business::inventory::orderTemplate'
Permission.Subject.Business.inventory_supplier; // 'business::inventory::supplier'
Permission.Subject.Business.inventory_warehouse; // 'business::inventory::warehouse'
Permission.Subject.Business.inventory_publish; // 'business::inventory::publish'
Permission.Subject.Business.inventory_import; // 'business::inventory::import'
Permission.Subject.Business.inventory_integration; // 'business::inventory::integration'

// HRM
Permission.Subject.Business.hrm_employee; // 'business::hrm::employee'
Permission.Subject.Business.hrm_teamMember; // 'business::hrm::teamMember'
Permission.Subject.Business.hrm_auditLog; // 'business::hrm::auditLog'

// Report
Permission.Subject.Business.report_createReport; // 'business::report::createReport'
Permission.Subject.Business.report_accessOverview; // 'business::report::accessOverview'
Permission.Subject.Business.report_accessInsight; // 'business::report::accessInsight'
Permission.Subject.Business.report_accessSetting; // 'business::report::accessSetting'
Permission.Subject.Business.report_accessIntegration; // 'business::report::accessIntegration'
Permission.Subject.Business.report_reports_allDefaultReports; // 'business::report::allDefaultReports'
Permission.Subject.Business.report_reports_allCustomReports; // 'business::report::allCustomReports'

checkAccess

import { checkAccess } from '@feedmepos/hrm-permission';
import type { RawRule } from '@casl/ability';

// userPermissions comes from your permission service / store
const userPermissions: RawRule[] = /* ... */;

Basic check (AND — all must pass)

const result = checkAccess(
	[
		{ action: PermissionAction.manage, subject: Permission.Subject.Business.hrm_teamMember },
		{ action: PermissionAction.read, subject: Permission.Subject.Business.hrm_employee },
	],
	userPermissions
);

if (result.granted) {
	// allowed
} else {
	console.log('blocked by', result.decisivePermission);
}

coverSubject — OR fallback

Grants access when the user can perform the action on either subject or coverSubject. Useful for "group covers individual" patterns (e.g. a catch-all report permission covering a specific dynamic report subject).

const result = checkAccess(
	[
		{
			action: PermissionAction.read,
			subject: 'business::report::myDynamicReport',
			coverSubject: Permission.Subject.Business.report_reports_allDefaultReports,
		},
	],
	userPermissions
);

Condition-aware check (single permission + subject instance)

Use this when a rule carries a $in / $eq condition (e.g. restaurant-scoped permissions). Only accepts a single permission because conditions are subject-specific.

const result = checkAccess(
	{ action: PermissionAction.manage, subject: Permission.Subject.Business.restaurant },
	userPermissions,
	{ restaurantId: 'abc' } // evaluated against rule conditions, e.g. { restaurantId: { $in: ['abc', 'def'] } }
);

// Typical use: filter a list to items the user can access
const accessible = restaurants.filter(
	(r) =>
		checkAccess(
			{ action: PermissionAction.manage, subject: Permission.Subject.Business.restaurant },
			userPermissions,
			{ restaurantId: r.id }
		).granted
);

Return value

interface AccessCheckResult {
	granted: boolean;
	/** The permission requirement that was decisive. */
	decisivePermission: CaslPermission;
	/** The matched CASL rule, or null if nothing matched. Parse `.reason` as JSON for audit source info. */
	decisiveRule: RawRule | null;
}

Adding a New Permission

Tip: You can ask Copilot to do this for you using the add-new-permission skill. Example prompts:

@workspace /add-new-permission add hrm_payroll under the HRM category with manage action
@workspace /add-new-permission add crm_loyaltyCard under CRM category, manage action, behind feature flag crm_loyalty_card
@workspace /add-new-permission add report_exportCsv under Report category with manage action

Adding a portal permission (one admins can toggle in the UI) requires two file changes.

Step 1 — Add the subject enum value

File: packages/permission/src/common/types.ts

export enum PermissionSubjectBusinessNamespace {
	// ... existing entries ...

	// HRM module — follow the module_feature naming pattern
	hrm_payroll = 'business::hrm::payroll',
}

Naming rules:

  • Prefix with the module: crm_, inventory_, hrm_, report_
  • camelCase feature name
  • Value pattern: business::<module>::<feature>

Step 2 — Add it to FullPortalPermissions

File: packages/permission/src/common/full-portal-permissions.ts

export const FullPortalPermissions = {
	// ... existing entries ...

	payroll: {
		label: 'Payroll Management', // shown in the UI permission editor and audit logs
		subject: PermissionSubjectBusinessNamespace.hrm_payroll,
		actions: [PermissionAction.manage],
		category: PermissionCategory.hrm, // General | Inventory | HRM | CRM | Report
		// showByFeatureFlag: 'payroll-beta',                // optional: hides checkbox until flag is on
	},
};

Available categories: PermissionCategory.general, .inventory, .hrm, .crm, .payment, .report, .reports, .customReports, .menu

That's it. The permission will now appear as a checkbox in the HRM portal permission editor under the correct category section.

Step 3 — Rebuild the package

cd packages/permission && pnpm build

Some features silently require access to other resources to work correctly (e.g. "Team Member Management" needs to read the POS role list to populate a dropdown). Rather than requiring admins to grant both, you define a system permission set that auto-injects the dependency whenever the parent permission is granted.

When to use this

  • Your feature silently requires read access to another resource → use permissions[]
  • You split an existing subject into multiple new granular subjects and existing users should silently get them → use permissionSets[] (backward-compat chaining)
  • If neither applies, skip this — most portal permissions do not need a system set

Subject namespaces in permissions[]

permissions[] accepts both resource subjects (hrm::posRole) and business/module subjects (business::payment::payoutAccount). Both are injected as leaf-level CASL rules and are never shown in the portal UI. Use PermissionSubjectBusinessNamespace directly when the subject already exists there — no separate resource enum needed. Create a resource enum only for subjects with no corresponding portal entry.

permissions[] — inject a read-only dependency

// permission-manifest/hrm/hrm-system-sets.ts
import { HrmResource } from './types';

[`set_${PermissionSubjectBusinessNamespace.hrm_teamMember}`]: {
  key: 'sys:team_member_access',   // stable — never change after deploy, stored in audit logs
  name: 'Team Member Access',      // shown in audit log breadcrumbs
  permissions: [
    {
      label: 'Pos Role',           // subject name only, reused as audit log display label
      subject: HrmResource.hrm_posRole,
      actions: [PermissionAction.read],
    },
  ],
},

Recommended rule: prefer read-only actions in permissions[] — avoid manage where possible. Injecting manage causes two problems: (1) UI bleed — the editor filters out system-injected entries by checking for non-manage actions, so a manage entry renders as an auto-checked checkbox; (2) over-grantmanage is a CASL wildcard covering all actions, silently granting destructive write access the admin never explicitly authorised. If manage access is needed as a side-effect, prefer permissionSets[] chaining instead. Some existing system sets (e.g. hrm-system-sets.ts) inject manage via permissions[] for legacy reasons — new sets should avoid this pattern.

permissionSets[] — chain to another portal permission (backward-compat)

Use when an existing parent permission should silently unlock new granular sibling subjects. Each chained subject gets a synthetic manage rule and its own system set is recursively expanded.

[`set_${PermissionSubjectBusinessNamespace.hrm_payroll}`]: {
  key: 'sys:payroll_access',
  name: 'Payroll Access',
  permissionSets: [
    PermissionSubjectBusinessNamespace.hrm_payroll_approval, // users with payroll:manage get this too
  ],
},

File organisation

When a domain has many system sets, extract them:

permission-manifest/
  hrm/
    types.ts           ← HrmResource enum
    hrm-system-sets.ts
    index.ts
  inventory/
    types.ts           ← InventoryResource enum
    inventory-system-sets.ts
    index.ts

Spread them into SYSTEM_PERMISSION_SETS in system-permission-sets.ts:

export const SYSTEM_PERMISSION_SETS = {
	...INVENTORY_SYSTEM_SETS,
	...HRM_SYSTEM_SETS,
};

Updated checklist (with system set)

| Step | File | Portal permission | Resource dependency | | ------------------------------- | ------------------------------- | :---------------: | :-----------------: | | Add enum value | types.ts | ✅ | ✅ | | Add to FullPortalPermissions | full-portal-permissions.ts | ✅ | ❌ | | Add to SYSTEM_PERMISSION_SETS | permission-manifest/<domain>/ | ❌ | ✅ | | Add @Action to controller | your controller | ✅ | ✅ | | Rebuild package | — | ✅ | ✅ |

import '@feedmepos/hrm-permission/style.css';

PermissionWrapper

Renders children only when route permissions are satisfied.

<template>
	<PermissionWrapper>
		<div>Protected content</div>
	</PermissionWrapper>
</template>

<script setup lang="ts">
import { PermissionWrapper } from '@feedmepos/hrm-permission/components';
</script>

withPermission HOC

Wraps a component so it renders only when the route's validationManifest permissions are satisfied. Typically combined with a lazy-loaded component:

import { withPermission } from '@feedmepos/hrm-permission/components';
import { withLoading } from '@/components/loading';
import { Permission } from '@feedmepos/hrm-permission';
import type { RouteMeta } from 'vue-router';

// Wrap lazy-loaded views
const hrMain = withPermission(withLoading(() => import('@/views/hr/Main.vue')));
const teamMain = withPermission(withLoading(() => import('@/views/team/Main.vue')));

// Helper to build the required route meta
const canManage = (subject: string): RouteMeta =>
	({
		validationManifest: {
			requiredCaslPermissions: [{ action: Permission.Action.manage, subject }],
		},
	}) as unknown as RouteMeta;

// Routes
const routes = [
	{
		path: '/',
		component: hrMain,
		meta: canManage(Permission.Subject.Business.hrm_employee),
		children: [
			{
				path: 'employee',
				component: withLoading(() => import('@/views/hr/employee/EmployeeList.vue')),
			},
			{ path: 'role', component: withLoading(() => import('@/views/hr/role/RoleList.vue')) },
		],
	},
	{
		path: '/team',
		component: teamMain,
		meta: canManage(Permission.Subject.Business.hrm_teamMember),
	},
];

The validationManifest on the route meta is read by withPermission to check the user's CASL ability before rendering. If the check fails, the component is not mounted.

Standalone

import { PermissionService } from '@feedmepos/hrm-permission/nestjs';

const service = new PermissionService({
	mongoUrl: process.env.MONGODB_URL,
	dbName: process.env.MONGODB_NAME || 'companyDB',
});

await service.initialize();

const ability = await service.constructAbility({
	userId: 'user123',
	level: 1, // Permission.Level.business
	role: 'admin',
	businessId: 'biz123',
});

await service.close();

NestJS module

// app.module.ts
import { PermissionModule } from '@feedmepos/hrm-permission/nestjs';

@Module({
	imports: [
		PermissionModule.forRoot({
			mongoUrl: process.env.MONGODB_URL,
			dbName: process.env.MONGODB_NAME,
		}),
	],
})
export class AppModule {}

// your.service.ts
import { PermissionService } from '@feedmepos/hrm-permission/nestjs';

@Injectable()
export class MyService {
	constructor(private readonly permissionService: PermissionService) {}
}

Environment variables

| Variable | Required | Description | | -------------- | -------- | ------------------------------------ | | MONGODB_URL | ✅ | MongoDB connection string | | MONGODB_NAME | — | Database name (default: companyDB) |

Audit logging is handled automatically by hrm-backend — every ActionGuard permission check writes a ClickHouse entry via gRPC. Consumer services do not call any audit-log function directly.

If you need to build a log entry manually (e.g. inside hrm-backend itself), use buildPermissionLog:

import { buildPermissionLog, type AuditContext } from '@feedmepos/hrm-permission/audit-log';
import { checkAccess } from '@feedmepos/hrm-permission/utils';

const result = checkAccess(
	[{ action: PermissionAction.read, subject: Permission.Subject.Business.hrm_employee }],
	userPermissions
);

const logEntry = buildPermissionLog(result, {
	userId: 'user123',
	requestPath: '/api/employees',
	requestMethod: 'GET',
	requestBody: '{"key":"value"}', // serialized string — plain JSON or "gzip:<base64>" for large bodies
	businessId: 'biz456',
	country: 'MY',
} satisfies AuditContext);

AuditContext.requestBody is a string — either plain JSON (JSON.stringify(body)) for bodies ≤ 1 MB, or "gzip:<base64>" for larger bodies (produced by serializeRequestBody in @feedmepos/hrm-actionguard). Legacy records stored in ClickHouse may have requestBody as a Record<string, unknown> object — AuditLogMetadata.requestBody is typed as string | Record<string, unknown> to handle both.

Flat → namespaced subjects

PermissionSubjectBusiness is @deprecated. Use PermissionSubjectBusinessNamespace instead. Legacy subjects are automatically remapped at runtime.

| Old (deprecated) | New | | --------------------------- | -------------------------------------- | | business::promotion | business::crm::promotion | | business::voucher | business::crm::voucher | | business::membership | business::crm::membership | | business::stock | business::inventory::stock | | business::ingredient | business::inventory::ingredient | | business::recipe | business::inventory::recipe | | business::unit | business::inventory::unit | | business::supplier | business::inventory::supplier | | business::warehouse | business::inventory::warehouse | | business::publish | business::inventory::publish | | business::integration | business::inventory::integration | | business::orderDraft | business::inventory::orderDraft | | business::wastageTemplate | business::inventory::wastageTemplate | | business::closingTemplate | business::inventory::closingTemplate | | business::orderTemplate | business::inventory::orderTemplate | | business::unitCostHistory | business::inventory::unitCostHistory | | business::permission | business::hrm::teamMember | | business::role | business::hrm::employee::role |

hasAccesscheckAccess

// Before
const canAccess: boolean = hasAccess(requiredPermissions, userPermissions);

// After
const result = checkAccess(requiredPermissions, userPermissions);
const canAccess = result.granted;

| Entry point | What's in it | | -------------------------------------- | ------------------------------------------------------------------------------------- | | @feedmepos/hrm-permission | Enums, types, constants, checkAccess | | @feedmepos/hrm-permission/utils | checkAccess, validate, validateRoute, mergePermissions, mapLegacyPermission | | @feedmepos/hrm-permission/components | PermissionWrapper, withPermission | | @feedmepos/hrm-permission/nestjs | PermissionModule, PermissionService + re-exports common | | @feedmepos/hrm-permission/service | PermissionService (standalone, no NestJS) | | @feedmepos/hrm-permission/audit-log | buildPermissionLog, AuditContext type | | @feedmepos/hrm-permission/style.css | Component styles |

Peer dependencies

  • @casl/ability
  • @feedmepos/core
  • @feedmepos/zod-repo
  • mongodb
  • vue ^3.5.0 (for components)
  • vue-router ^4.2.0 (for route validation)
pnpm install

pnpm dev        # run demo app with hot reload
pnpm build      # build library
pnpm type-check