npm package: @feedmepos/hrm-permission
Description: Unified permission management package for FeedMe HRM system with both frontend (Vue) and backend (NestJS/standalone) support. Provides type-safe permission definitions, CASL-based validation, Vue components, a MongoDB-backed permission service, and ClickH
Latest version: 1.0.1

# @feedmepos/hrm-permission

Unified permission management package for FeedMe HRM system with both frontend (Vue) and backend (NestJS/standalone) support. Provides type-safe permission definitions, CASL-based validation, Vue components, a MongoDB-backed permission service, and ClickHouse-based audit logging.

## Features

- 📋 **Type-Safe Permissions**: Comprehensive namespaced permission enums and types for FeedMe, Business, and Restaurant levels
- 🔐 **CASL Integration**: Validate user permissions using CASL ability framework with detailed access check results
- 🎯 **Route Protection**: Protect Vue Router routes with declarative permission checks
- 🧩 **Vue Components**: Ready-to-use `PermissionWrapper` component and `withPermission` HOC
- 🚀 **Standalone Service**: Framework-agnostic MongoDB-based permission service with `PermissionAssignment` and `PermissionSet` support
- 🏢 **NestJS Module**: Optional NestJS integration via `PermissionModule`
- 📦 **Modular Exports**: Import only what you need from subpaths
- 🔄 **Legacy Compatibility**: Automatic mapping from old flat permission subjects to new namespaced subjects
- 📊 **Audit Logging**: ClickHouse-based permission check audit logging

## Installation

```bash
pnpm add @feedmepos/hrm-permission
# or
npm install @feedmepos/hrm-permission
# or
yarn add @feedmepos/hrm-permission
```

## Peer Dependencies

- `@casl/ability` - Permission validation
- `@feedmepos/core` - FeedMe entity definitions
- `@feedmepos/zod-repo` - MongoDB repository layer
- `mongodb` - Database client
- `vue` ^3.5.0 (for components)
- `vue-router` ^4.2.0 (for route validation)

## Package Structure

This package provides multiple entry points:

- **Main** (`@feedmepos/hrm-permission`) - Permission types and enums
- **Components** (`@feedmepos/hrm-permission/components`) - Vue components
- **Utils** (`@feedmepos/hrm-permission/utils`) - Validation, access check, permission-set merging, and audit log utilities
- **Entities** (`@feedmepos/hrm-permission/entities`) - Zod schemas for `FdoPermissionAssignment` and `FdoPermissionSet`
- **NestJS** (`@feedmepos/hrm-permission/nestjs`) - NestJS module and service
- **Styles** (`@feedmepos/hrm-permission/style.css`) - Component styles

## Usage

### 1. Permission Types & Enums

```typescript
// Import permission definitions
import {
	Permission,
	PermissionLevel,
	PermissionAction,
	PermissionSubjectBusinessNamespace,
	FullPortalPermissions,
	FullPosPermissions,
	LegacyPermissionMapping,
	type CaslPermission,
	type ValidationManifest,
	type TaggedPermissionRule,
	type FdoPermissionSet,
} from '@feedmepos/hrm-permission';

// Use permission enums (namespaced subjects)
const canManageMenu: CaslPermission = {
	action: Permission.Action.manage,
	subject: Permission.Subject.Business.menu, // 'business::menu'
};

// New namespaced business subjects
const canManagePromotion: CaslPermission = {
	action: Permission.Action.manage,
	subject: Permission.Subject.Business.crm_promotion, // 'business::crm::promotion'
};

const canManageTeam: CaslPermission = {
	action: Permission.Action.manage,
	subject: Permission.Subject.Business.hrm_teamMember, // 'business::hrm::teamMember'
};

// Check permission level
if (userLevel === Permission.Level.business) {
	// Business-level user
}
```

### 2. Vue Components

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

<template>
	<PermissionWrapper>
		<div>This content is protected by route permissions</div>
	</PermissionWrapper>
</template>

<script setup>
import { PermissionWrapper } from '@feedmepos/hrm-permission';
</script>
```

#### NoPermission

Display a "no permission" message to users without access.

```vue
<template>
	<NoPermission />
</template>

	<!-- Higher-order component usage -->
	<component :is="AdminButton" />
</template>

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

// Wrap component with permission check
const ProtectedButton = withPermission(AdminButton);
</script>
```

### 3. Permission Validation Utils

```typescript
import {
	validate,
	validateRoute,
	checkAccess,
	mergePermissions,
	mapLegacyPermission,
	mapToLegacyPermission,
} from '@feedmepos/hrm-permission/utils';
import { Permission } from '@feedmepos/hrm-permission';
import type { ValidationManifest } from '@feedmepos/hrm-permission';

// Programmatic validation
const result = await validate({
	sessionUser: currentUser,
	validationManifest: {
		requiredRoles: ['admin'],
		requiredCaslPermissions: [
			{
				action: Permission.Action.manage,
				subject: Permission.Subject.Business.menu,
			},
		],
		customValidationFunction: async () => {
			// Custom logic
			return { canAccess: true };
		},
	},
});

if (result.canAccess) {
	// User has permission
}

// Check access with CASL rules (AND gate — conditions NOT evaluated)
const accessResult = checkAccess(
	[
		{
			action: Permission.Action.update,
			subject: Permission.Subject.Business.menu,
		},
	],
	userPermissions
);

if (accessResult.granted) {
	// Access granted
	console.log('Decisive rule:', accessResult.decisiveRule);
} else {
	console.log('Denied by:', accessResult.decisivePermission);
}

// Condition-aware check — evaluates $in / $eq conditions against a subject instance.
// Only available for a single permission; use this when filtering resources by access rights.
const conditionResult = checkAccess(
	{
		action: Permission.Action.manage,
		subject: Permission.Subject.Restaurant.restaurant,
	},
	userPermissions,
	{ restaurantId: 'abc' } // evaluated against rule conditions, e.g. { restaurantId: { $in: ['abc', 'def'] } }
);

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

// Merge permissions from multiple permission sets
const merged = mergePermissions(permissionSets);
// merged[0].setNames  -> array of permission set names that granted this permission
// merged[0].setId     -> the permission set _id

// Map legacy flat subjects to namespaced subjects
const newSubject = mapLegacyPermission('business::promotion');
// -> 'business::crm::promotion'

// Map namespaced subjects back to legacy subjects
const legacySubject = mapToLegacyPermission('business::crm::promotion');
// -> 'business::promotion'
```

### 4. Route Protection

Add validation metadata to your routes:

```typescript
import { createRouter } from 'vue-router';
import { validateRoute } from '@feedmepos/hrm-permission/utils';
import { Permission, type ValidationManifest } from '@feedmepos/hrm-permission';

const router = createRouter({
	routes: [
		{
			path: '/menu',
			component: MenuManagement,
			meta: {
				validationManifest: {
					requiredRoles: ['admin', 'manager'],
					requiredCaslPermissions: [
						{
							action: Permission.Action.manage,
							subject: Permission.Subject.Business.menu,
						},
					],
				} as ValidationManifest,
			},
		},
	],
});

// Add route guard
router.beforeEach(async (to) => {
	const result = await validateRoute(to);

	if (!result.canAccess && result.redirectTo) {
		return { name: result.redirectTo };
	}
});
```

### 5. Backend Service (Standalone)

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

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

// Initialize (connect to MongoDB)
await permissionService.initialize();

// Construct user ability
const ability = await permissionService.constructAbility({
	userId: 'user123',
	level: 1, // Business level
	role: 'admin',
	businessId: 'business123',
});

// Get full portal permissions
const allPermissions = permissionService.getFullPermission();

// Query businesses
const businesses = await permissionService.getOwnerBusiness('ownerId');

// Close connection when done
await permissionService.close();
```

### 6. NestJS Integration

```typescript
import { Module } from '@nestjs/common';
import { PermissionModule } from '@feedmepos/hrm-permission/nestjs';

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

Then inject the service:

```typescript
import { Injectable } from '@nestjs/common';
import { PermissionService } from '@feedmepos/hrm-permission/nestjs';

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

	async checkUserPermissions(userId: string) {
		const ability = await this.permissionService.constructAbility({
			userId,
			level: 1,
			role: 'staff',
		});
		return ability;
	}
}
```

### 7. Audit Logging

Permission checks can be asynchronously logged to ClickHouse for audit and observability purposes.

> **Node.js only** — `useAuditLog()` relies on `process.env` and the `@clickhouse/client` Node.js driver.
> It must only be called from a **server-side (NestJS) context**, never from Vue/browser code.

> **Credentials required** — the logger is a **no-op** (`isActive === false`) when `CLICKHOUSE_URL` is not set.
> No errors are thrown and no inserts are attempted. Set all three env vars to activate logging:
> `CLICKHOUSE_URL`, `CLICKHOUSE_USER`, `CLICKHOUSE_PASSWORD`.

```typescript
import { checkAccess, buildPermissionLog, useAuditLog } from '@feedmepos/hrm-permission/utils';
import { Permission } from '@feedmepos/hrm-permission';

// In your NestJS guard or middleware:
const result = checkAccess(
	[{ action: Permission.Action.read, subject: Permission.Subject.Business.hrm_employee }],
	userPermissions
);

// Build a structured log entry
const logEntry = buildPermissionLog(result, {
	userId: 'user123',
	requestPath: '/api/employees',
	requestMethod: 'GET',
	businessId: 'biz456',
	country: 'MY',
});

// Fire-and-forget async insert to ClickHouse (no-op if CLICKHOUSE_URL is not set)
useAuditLog().log(logEntry);

// You can also check if the logger is active before logging
if (useAuditLog().isActive) {
	useAuditLog().log(logEntry);
}
```

> **ClickHouse DDL** (run once to create the table):
>
> ```sql
> CREATE TABLE hrm_permission_check_log (
>   timestamp    DateTime,
>   userId       String,
>   subject      String,
>   action       String,
>   field        Nullable(String),
>   businessId   Nullable(String),
>   restaurantId Nullable(String),
>   country      Nullable(String),
>   outcome      Enum8('allowed' = 1, 'denied' = 2),
>   metadata     String   -- JSON: { reason, resolvedFrom, permissionSetId?, requestPath?, requestMethod? }
> ) ENGINE = MergeTree()
> ORDER BY (timestamp, userId);
> ```

### 8. Entities (Zod Schemas)

```typescript
import { FdoPermissionAssignment, FdoPermissionSet } from '@feedmepos/hrm-permission/entities';
import type {
	FdoPermissionAssignment as FdoPermissionAssignmentType,
	FdoPermissionSet as FdoPermissionSetType,
} from '@feedmepos/hrm-permission/entities';

// FdoPermissionAssignment schema
// {
//   _id: ObjectId string
//   customPermissions: FdoPermissionRule[]
//   permissionSetIds: string[]
//   businessId: string
//   userId: string
// }

// FdoPermissionSet schema
// {
//   _id: ObjectId string
//   permissions: FdoPermissionRule[]
//   businessId: string
//   name: string
// }
```

## Permission Structure

### Permission Levels

```typescript
Permission.Level.feedMe; // 0 - FeedMe admin level
Permission.Level.business; // 1 - Business level
Permission.Level.restaurant; // 2 - Restaurant level
```

### Permission Actions

```typescript
Permission.Action.manage; // Full control
Permission.Action.create; // Create resources
Permission.Action.read; // Read resources
Permission.Action.update; // Update resources
Permission.Action.delete; // Delete resources
```

### Permission Subjects

**FeedMe Level:**

```typescript
Permission.Subject.Feedme.all;
Permission.Subject.Feedme.restaurant;
Permission.Subject.Feedme.business;
Permission.Subject.Feedme.delivery;
Permission.Subject.Feedme.agent;
```

**Business Level** (namespaced by module):

```typescript
// General
Permission.Subject.Business.profile; // 'business::profile'
Permission.Subject.Business.restaurant; // 'business::restaurant'
Permission.Subject.Business.menu; // 'business::menu'

// CRM module
Permission.Subject.Business.crm; // 'business::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'

// Inventory module
Permission.Subject.Business.inventory; // 'business::inventory'
Permission.Subject.Business.inventory_stock; // 'business::inventory::stock'
Permission.Subject.Business.inventory_ingredient; // 'business::inventory::ingredient'
Permission.Subject.Business.inventory_recipe; // 'business::inventory::recipe'
Permission.Subject.Business.inventory_unit; // 'business::inventory::unit'
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_integration; // 'business::inventory::integration'
Permission.Subject.Business.inventory_orderDraft; // 'business::inventory::orderDraft'
Permission.Subject.Business.inventory_wastageTemplate; // 'business::inventory::wastageTemplate'
Permission.Subject.Business.inventory_closingTemplate; // 'business::inventory::closingTemplate'
Permission.Subject.Business.inventory_orderTemplate; // 'business::inventory::orderTemplate'
Permission.Subject.Business.inventory_unitCostHistory; // 'business::inventory::unitCostHistory'

// HRM module
Permission.Subject.Business.hrm; // 'business::hrm'
Permission.Subject.Business.hrm_employee; // 'business::hrm::employee'
Permission.Subject.Business.hrm_employee_list; // 'business::hrm::employee::list'
Permission.Subject.Business.hrm_employee_role; // 'business::hrm::employee::role'
Permission.Subject.Business.hrm_employee_timesheet; // 'business::hrm::employee::timesheet'
Permission.Subject.Business.hrm_teamMember; // 'business::hrm::teamMember'
Permission.Subject.Business.hrm_teamMember_list; // 'business::hrm::teamMember::list'
Permission.Subject.Business.hrm_teamMember_role; // 'business::hrm::teamMember::role'
```

> **Note:** The old flat `PermissionSubjectBusiness` enum (e.g. `business::promotion`, `business::stock`) is `@deprecated` but still supported via `LegacyPermissionMapping` for backward compatibility.

## Adding a New Permission

This section is the starting point for any team adding a new feature that needs access control. The system has two distinct concepts that are often confused — understand the distinction first, then follow the checklist.

---

### Concepts at a Glance

| Concept                              | File                         | Role                                                                                                                                                                                           |
| ------------------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PermissionSubjectBusinessNamespace` | `types.ts`                   | The canonical enum for every subject string in the system. **Always start here.**                                                                                                              |
| `FullPortalPermissions`              | `full-portal-permissions.ts` | Subjects that appear as toggles in the HRM portal permission editor UI. Each entry includes a `category` field that controls which UI section it appears under (General, Inventory, HRM, CRM). |
| `SYSTEM_PERMISSION_SETS`             | `system-permission-sets.ts`  | Auto-injected side-effects. When a user holds `manage` on subject X, the system silently grants additional capabilities defined in `set_X`.                                                    |

---

### Step 1 — Add a subject to `PermissionSubjectBusinessNamespace`

**Every** new controllable resource starts here, regardless of type. This is the single source of truth for subject strings.

**Naming rule:** `module_feature` or `module_feature_subfeature`, using the `business::<module>::<feature>` pattern for the value.

```typescript
// types.ts
export enum PermissionSubjectBusinessNamespace {
	// — existing entries —

	// HRM module
	hrm_payroll = 'business::hrm::payroll', // new top-level feature
	hrm_payroll_approval = 'business::hrm::payroll::approval', // granular sub-feature
}
```

**Naming rules:**

- Use the module prefix: `crm_`, `inventory_`, `hrm_`
- Use camelCase for the feature name
- Sub-resources that are never directly assignable by admins but are read-access dependencies (e.g. `hrm_employee_role`) go here too — just do not add them to `FullPortalPermissions`
- Never reuse or repurpose an existing subject string for a new concept

---

### Step 2 — Decide: Portal Permission or Resource Dependency?

Ask: **"Should an admin be able to assign this permission directly in the UI?"**

```
Can admins toggle this subject ON/OFF directly in the permission editor?
├── YES → Portal Permission   → go to Step 3
└── NO  → Resource Dependency → go to Step 4
```

**Portal permission** = a user-facing feature page or module (e.g. "Payroll Management").

**Resource dependency** = a supporting subject another feature silently needs to function (e.g. `hrm_employee_role:read` needed for team member management to display role names).

---

### Step 3 — Portal Permission: add to `FullPortalPermissions`

`FullPortalPermissions` controls what checkboxes appear in the permission editor. The `category` field on each entry controls which UI section it appears under. TypeScript will error if you omit `category`.

```typescript
// full-portal-permissions.ts
export const FullPortalPermissions = {
	// ... existing entries ...

	payroll: {
		label: 'Payroll Management', // shown in UI and audit log
		subject: PermissionSubjectBusinessNamespace.hrm_payroll,
		actions: [PermissionAction.manage],
		category: PermissionCategory.hrm, // General | Inventory | HRM | CRM
		// showByFeatureFlag: 'payroll-beta',       // optional: hide behind a feature flag
	},
};
```

**Label convention:** `"<Feature> Management"` — this label is shown in the permission editor UI and in audit log entries. Keep it short and human-readable.

> If your feature is behind a feature flag and should not yet be visible in the UI, set `showByFeatureFlag: 'your-flag-name'` on the entry. The permission is still valid in the system; only the UI checkbox is hidden.

---

### Step 4 — Resource Dependency: add to `SYSTEM_PERMISSION_SETS`

Resource dependencies are auto-injected when a user is granted `manage` on a related portal permission subject. They are currently not shown in the UI for granular control.

A system set is keyed as `set_<subject>` and activates whenever a user holds `manage` on that subject.

Create a folder under permission-manifest/ base on domain, /inventory, /crm etc, to extract out permission definition use in system permission set if you have a long list of permission, to avoid bloating system-permission-sets file.

#### `permissions` — inject a granular access dependency, read, update, delete etc

Use this when your feature **silently requires** read access to another resource to function correctly (e.g. displaying a dropdown of roles).

```typescript
// system-permission-sets.ts
[`set_${PermissionSubjectBusinessNamespace.hrm_teamMember}`]: {
  key: 'sys:team_member_access',    // stable, never change after first use
  name: 'Team Member Access',       // appears in audit log breadcrumbs
  permissions: [
    {
      label: 'POS Role',            // subject-centric, used in audit log display
      subject: PermissionSubjectBusinessNamespace.hrm_employee_role,
      actions: [PermissionAction.read],
    },
  ],
},
```

**Label convention for `permissions` entries:** use the subject name only, not the action — `"POS Role"` not `"POS Role (read)"`. This label is reused as the audit log subject display name.

#### `permissionSets` — chain to another portal permission subject

Use this for backward compatibility when you split an existing subject into multiple new granular subjects, and existing users should silently gain the new subjects they would have had access to under the old model.

```typescript
// system-permission-sets.ts
[`set_${PermissionSubjectBusinessNamespace.hrm_payroll}`]: {
  key: 'sys:payroll_access',
  name: 'Payroll Access',
  permissionSets: [
    // Users with payroll:manage automatically get manage on these too
    PermissionSubjectBusinessNamespace.hrm_payroll_approval,
  ],
},
```

Each subject listed in `permissionSets` will have its own system set recursively expanded (if one exists). A `visitedSetIds` guard prevents infinite loops.

**`key` naming rule:** `sys:<module>_<feature>_access`, all lowercase, hyphens or underscores only. **Never change a `key` after it has been deployed** — it is stored in audit log records.

---

### Step 5 — Guard the backend endpoint

On the NestJS controller, apply the `@Action` decorator with the subject you registered:

```typescript
@Action({
  level: Permission.Level.business,
  subject: Permission.Subject.Business.hrm_payroll,
  action: Permission.Action.manage,
})
@Get('/payroll')
async getPayroll() { ... }
```

---

### Complete Checklist

| Step                                                | File                         | Required for portal permission | Required for resource dependency |
| --------------------------------------------------- | ---------------------------- | ------------------------------ | -------------------------------- |
| 1. Add enum value                                   | `types.ts`                   | ✅                             | ✅                               |
| 2. Add to `FullPortalPermissions` (with `category`) | `full-portal-permissions.ts` | ✅                             | ❌                               |
| 3. Add to `SYSTEM_PERMISSION_SETS.permissions`      | `system-permission-sets.ts`  | ❌                             | ✅                               |
| 4. Add `@Action` to controller                      | your controller              | ✅                             | ✅                               |
| 5. Rebuild the `@feedmepos/hrm-permission` package  | —                            | ✅                             | ✅                               |

---

### Frequently Asked Questions

**Q: A subject appears in both `FullPortalPermissions` and in a system set's `permissions`. Is that correct?**

Yes. `inventory_stock` for example is a portal permission (admins can directly grant it) and also a read-dependency injected by `set_inventory_orderDraft`. The two roles are independent — `FullPortalPermissions` controls UI visibility; system sets control auto-injection.

**Q: My new feature needs both a UI toggle and a read dependency on another subject. How?**

Do both: add an entry to `FullPortalPermissions` (Step 3) and add a `SYSTEM_PERMISSION_SETS` entry for your new subject with a `permissions` array containing the dependency (Step 4).

**Q: Can I add a new `PermissionCategory` for a new module?**

Yes. Add a new key to the `PermissionCategory` const object in `types.ts`, then set `category` to the new value on the relevant entries in `FullPortalPermissions`.

**Q: Do I need a system set entry for every portal permission?**

No. Only add a system set entry if granting `manage` on your subject should silently unlock additional capabilities. Most portal permissions do not need a system set.

---

## Migration Guide

### Upgrading from Flat to Namespaced Permission Subjects

The `PermissionSubjectBusiness` enum is now `@deprecated`. Use `PermissionSubjectBusinessNamespace` (accessible via `Permission.Subject.Business`) instead. Legacy subjects are automatically mapped at runtime via `LegacyPermissionMapping`.

| Old (deprecated)            | New (namespaced)                       |
| --------------------------- | -------------------------------------- |
| `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`        |

### Upgrading from `hasAccess` to `checkAccess`

`hasAccess` has been removed. Replace usages with `checkAccess`, which returns a detailed `AccessCheckResult`:

```typescript
// Before
import { hasAccess } from '@feedmepos/hrm-permission/utils';
const canAccess: boolean = hasAccess(requiredPermissions, userPermissions);

// After
import { checkAccess } from '@feedmepos/hrm-permission/utils';
const result = checkAccess(requiredPermissions, userPermissions);
const canAccess: boolean = result.granted;
// result.decisivePermission — the permission that was decisive
// result.decisiveRule       — the matched CASL rule (or null)
```

## API Reference

### Types

```typescript
import type {
	// Core types
	CaslPermission,
	ValidationManifest,
	ValidationParams,
	ValidationResult,
	RouteValidationResult,
	Role,
	// Permission rule with source tracking
	TaggedPermissionRule,
	// Service types
	PermissionServiceConfig,
	AbilityConstructOption,
	PermissionLevelDetail,
	FdoPermissionSet,
	IFdtoPortalPermission,
	IPermissionUser,
	// Enums
	PermissionLevel,
	PermissionAction,
	PermissionSubjectFeedme,
	PermissionSubjectFeedmeBusinessField,
	PermissionSubjectBusinessNamespace,
	PermissionSubjectBusiness, // @deprecated
	PermissionSubjectRestaurant,
	// Constants
	FullPortalPermissions,
	FullPosPermissions,
	LegacyPermissionMapping,
} from '@feedmepos/hrm-permission';

// Entity types (from entities subpath)
import type {
	FdoPermissionAssignment,
	FdoPermissionSet as FdoPermissionSetEntity,
} from '@feedmepos/hrm-permission/entities';

// Audit log types (from utils subpath)
import type {
	AccessCheckResult,
	PermissionCheckLog,
	PermissionCheckMetadata,
	AuditContext,
} from '@feedmepos/hrm-permission/utils';
```

### Utility Functions

- **`checkAccess(requiredPermissions, userPermissions)`** - Check if user permissions satisfy requirements; returns `AccessCheckResult` with `granted`, `decisivePermission`, and `decisiveRule`
- **`validate(params)`** - Validate user permissions with manifest
- **`validateRoute(route)`** - Validate route with metadata
- **`mergePermissions(permissionSets)`** - Merge permissions from multiple `FdoPermissionSet` objects into a deduplicated list of `MergedPermission` (includes `setNames` and `setId`)
- **`mapLegacyPermission(subject)`** - Map a legacy flat permission subject to the new namespaced subject
- **`mapToLegacyPermission(subject)`** - Map a namespaced subject back to a legacy flat subject
- **`buildPermissionLog(result, context)`** - Build a `PermissionCheckLog` entry from a `checkAccess` result and request context
- **`useAuditLog()`** - Get the singleton `AuditLogger` instance for logging permission checks to ClickHouse. **Node.js only.** Returns a no-op logger (`isActive === false`) when `CLICKHOUSE_URL` is absent.

### Components

- **`PermissionWrapper`** - Wraps content, shows only if route permissions are satisfied
- **`withPermission(Component)`** - HOC to wrap a component with permission checks

### Service Methods

- **`initialize()`** - Connect to MongoDB (must be called before other methods)
- **`close()`** - Close MongoDB connection
- **`constructAbility(options)`** - Build CASL ability for a user (resolves permissions from `PermissionAssignment` and `PermissionUser`)
- **`getFullPermission()`** - Get all portal permissions (`FullPortalPermissions`)
- **`getOwnerBusiness(ownerId)`** - Query businesses by owner
- **`getPermissionFromPermissionAssignment(options)`** - Get tagged permission rules from a user's `PermissionAssignment` record (supports `customPermissions` and linked `permissionSetIds`)
- **`getPermissionFromPermissionUser(options)`** - Get tagged permission rules from a legacy `PermissionUser` record
- **`findById(userId)`** - Find permission user by ID
- **`findByBusinessId(businessId)`** - Find all permission users in a business
- **`findByUserId(userId)`** - Find all permission users by user ID
- **`create(user)`** - Create new permission user
- **`update(id, user)`** - Update permission user
- **`delete(id)`** - Delete permission user

## Environment Variables

When using `PermissionService`, configure via environment variables:

- **`MONGODB_URL`** - MongoDB connection string (required)
- **`MONGODB_NAME`** - Database name (default: `companyDB`)

When using the audit logger, configure ClickHouse via:

- **`CLICKHOUSE_URL`** - ClickHouse server URL (e.g. `http://localhost:8123`). **Required to activate logging** — if absent, the logger is a no-op and no errors are thrown.
- **`CLICKHOUSE_USER`** - ClickHouse username
- **`CLICKHOUSE_PASSWORD`** - ClickHouse password

> The audit logger only works in **Node.js environments**. Do not call `useAuditLog()` from Vue/browser code.

## Development

```bash
# Install dependencies
pnpm install

# Run demo app (with hot reload)
pnpm dev

# Build library for production
pnpm build

# Type check
pnpm type-check
```

### ClickHouse Migration

To create the ClickHouse table for audit logging, run the migration script once:

```bash
CLICKHOUSE_URL=http://localhost:8123 CLICKHOUSE_USER=default CLICKHOUSE_PASSWORD=... npx tsx scripts/migrate-clickhouse.ts
```

## Demo

The package includes a demo application showcasing the permission system. Run `pnpm dev` to see it in action.

## License

MIT