payload-rbac-plugin 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +177 -0
  3. package/dist/collections/Permissions.d.ts +3 -0
  4. package/dist/collections/Permissions.js +121 -0
  5. package/dist/collections/Permissions.js.map +1 -0
  6. package/dist/collections/Roles.d.ts +3 -0
  7. package/dist/collections/Roles.js +39 -0
  8. package/dist/collections/Roles.js.map +1 -0
  9. package/dist/components/BeforeDashboardClient.d.ts +1 -0
  10. package/dist/components/BeforeDashboardClient.js +40 -0
  11. package/dist/components/BeforeDashboardClient.js.map +1 -0
  12. package/dist/components/BeforeDashboardServer.d.ts +2 -0
  13. package/dist/components/BeforeDashboardServer.js +22 -0
  14. package/dist/components/BeforeDashboardServer.js.map +1 -0
  15. package/dist/components/BeforeDashboardServer.module.css +5 -0
  16. package/dist/endpoints/customEndpointHandler.d.ts +2 -0
  17. package/dist/endpoints/customEndpointHandler.js +7 -0
  18. package/dist/endpoints/customEndpointHandler.js.map +1 -0
  19. package/dist/exports/client.d.ts +1 -0
  20. package/dist/exports/client.js +3 -0
  21. package/dist/exports/client.js.map +1 -0
  22. package/dist/exports/rsc.d.ts +1 -0
  23. package/dist/exports/rsc.js +3 -0
  24. package/dist/exports/rsc.js.map +1 -0
  25. package/dist/hooks/beforePermissionChange.d.ts +2 -0
  26. package/dist/hooks/beforePermissionChange.js +58 -0
  27. package/dist/hooks/beforePermissionChange.js.map +1 -0
  28. package/dist/index.d.ts +5 -0
  29. package/dist/index.js +38 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/types.d.ts +47 -0
  32. package/dist/types.js +3 -0
  33. package/dist/types.js.map +1 -0
  34. package/dist/utilities/checkPermission.d.ts +8 -0
  35. package/dist/utilities/checkPermission.js +16 -0
  36. package/dist/utilities/checkPermission.js.map +1 -0
  37. package/dist/utilities/hasPermission.d.ts +10 -0
  38. package/dist/utilities/hasPermission.js +32 -0
  39. package/dist/utilities/hasPermission.js.map +1 -0
  40. package/dist/utilities/hasPermission.spec.d.ts +1 -0
  41. package/dist/utilities/hasPermission.spec.js +122 -0
  42. package/dist/utilities/hasPermission.spec.js.map +1 -0
  43. package/dist/utilities/index.d.ts +2 -0
  44. package/dist/utilities/index.js +4 -0
  45. package/dist/utilities/index.js.map +1 -0
  46. package/package.json +142 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mahmoud Hassan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # Payload CMS Dynamic RBAC Plugin
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/payload-rbac-plugin.svg?style=flat-square)](https://www.npmjs.com/package/payload-rbac-plugin)
4
+ [![NPM Downloads](https://img.shields.io/npm/dm/payload-rbac-plugin.svg?style=flat-square)](https://www.npmjs.com/package/payload-rbac-plugin)
5
+ [![License](https://img.shields.io/npm/l/payload-rbac-plugin.svg?style=flat-square)](https://github.com/Mhmod-Hsn/Payload-RBAC-Plugin/blob/main/LICENSE)
6
+ [![CI Status](https://img.shields.io/github/actions/workflow/status/Mhmod-Hsn/Payload-RBAC-Plugin/ci.yml?branch=main&style=flat-square)](https://github.com/Mhmod-Hsn/Payload-RBAC-Plugin/actions)
7
+
8
+ A professional, database-backed Role-Based Access Control (RBAC) system for [Payload CMS](https://payloadcms.com) (v3).
9
+
10
+ This plugin emphasizes checking **permissions instead of roles** to avoid hard-coded authorization logic in your source code, making your access control highly scalable, modular, and dynamic.
11
+
12
+ ---
13
+
14
+ ## Features
15
+
16
+ - **Dynamic Collections**: Automatically injects database-backed `Roles` and `Permissions` collections.
17
+ - **Auth Collection Extension**: Injects a `roles` relationship field into your target auth collection (e.g., `users`) with `saveToJWT: true` for zero-cost runtime checks.
18
+ - **Bulk Permission Generator**: In the Admin UI, easily switch between creating a single permission or using the Bulk CRUD Generator to automatically generate separate permissions (e.g., `posts:create`, `posts:read`, `posts:update`, `posts:delete`) at once.
19
+ - **UI-Only Helpers**: Bulk generator fields (`type`, `collectionName`, CRUD checkboxes) are purely for the UI and are never saved to your database, keeping your collections clean.
20
+ - **Customizable Schemas**: Allow developers to inject additional custom fields into both `Roles` and `Permissions` schemas.
21
+ - **Granular Control**: Set visibility permissions (`hideRoles`, `hidePermissions`) dynamically or statically for the RBAC admin interface.
22
+ - **Helper Utilities**: Simple and robust functions to check permissions (`hasPermission`) and access control wrappers (`checkPermission`).
23
+ - **Fully Tested**: Powered by a robust integration and unit test suite built with Vitest.
24
+
25
+ ## Screenshots
26
+
27
+ ### 1. Permissions List View
28
+ Shows flat generated permissions (e.g. `posts:create`, `posts:read`) and legacy system permissions.
29
+ ![Permissions List View](images/permissions_list.png)
30
+
31
+ ### 2. Single Permission Form
32
+ Create individual custom permissions.
33
+ ![Single Permission Form](images/permissions_single.png)
34
+
35
+ ### 3. Bulk CRUD Generator Form
36
+ Select a collection and check CRUD actions to instantly generate multiple permissions.
37
+ ![Bulk CRUD Generator Form](images/permissions_bulk.png)
38
+
39
+ ### 4. Roles List View
40
+ Displays roles and all permissions associated with them.
41
+ ![Roles List View](images/roles_list.png)
42
+
43
+ ### 5. Role Configuration Form
44
+ Assign permissions directly to roles in the admin UI.
45
+ ![Role Configuration Form](images/role_edit.png)
46
+
47
+ ---
48
+
49
+ ## Installation
50
+
51
+ Add the plugin to your project:
52
+
53
+ ```bash
54
+ pnpm add payload-rbac-plugin
55
+ # or
56
+ npm install payload-rbac-plugin
57
+ # or
58
+ yarn add payload-rbac-plugin
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Setup & Configuration
64
+
65
+ Import and configure the plugin in your `payload.config.ts`:
66
+
67
+ ```typescript
68
+ import { buildConfig } from 'payload'
69
+ import { rbac, hasPermission } from 'payload-rbac-plugin'
70
+
71
+ export default buildConfig({
72
+ collections: [
73
+ {
74
+ slug: 'posts',
75
+ fields: [],
76
+ },
77
+ ],
78
+ plugins: [
79
+ rbac({
80
+ enabled: true,
81
+ // Optional configurations:
82
+ authCollectionSlug: 'users', // Default: 'users'
83
+ rolesCollectionSlug: 'roles', // Default: 'roles'
84
+ permissionsCollectionSlug: 'permissions', // Default: 'permissions'
85
+
86
+ // Control access/visibility of the RBAC panel:
87
+ hidePermissions: ({ user }) => !hasPermission(user, 'access:permissions'),
88
+ hideRoles: ({ user }) => !hasPermission(user, 'access:roles'),
89
+ }),
90
+ ],
91
+ })
92
+ ```
93
+
94
+ ### Configuration Options
95
+
96
+ | Option | Type | Default | Description |
97
+ | :--- | :--- | :--- | :--- |
98
+ | `enabled` | `boolean` | `true` | Enable or disable the plugin. |
99
+ | `authCollectionSlug` | `string` | `'users'` | The collection slug representing the users collection. |
100
+ | `rolesCollectionSlug` | `string` | `'roles'` | The slug for the automatically injected Roles collection. |
101
+ | `permissionsCollectionSlug` | `string` | `'permissions'` | The slug for the automatically injected Permissions collection. |
102
+ | `rolesFields` | `Field[]` | `[]` | Extra custom fields to inject into the Roles collection. |
103
+ | `permissionsFields` | `Field[]` | `[]` | Extra custom fields to inject into the Permissions collection. |
104
+ | `hideRoles` | `boolean \| ((args: { user: any }) => boolean)` | `false` | Hide the Roles collection from the sidebar navigation. |
105
+ | `hidePermissions` | `boolean \| ((args: { user: any }) => boolean)` | `false` | Hide the Permissions collection from the sidebar navigation. |
106
+
107
+ ---
108
+
109
+ ## Usage
110
+
111
+ ### 1. Protecting Collections (Access Control)
112
+
113
+ You can protect collections using the `checkPermission` Higher-Order Function. It returns a standard Payload `Access` control resolver.
114
+
115
+ ```typescript
116
+ import { checkPermission } from 'payload-rbac-plugin'
117
+
118
+ export const PostsCollection = {
119
+ slug: 'posts',
120
+ access: {
121
+ create: checkPermission('posts:create'),
122
+ read: checkPermission('posts:read'),
123
+ update: checkPermission('posts:update'),
124
+ delete: checkPermission('posts:delete'),
125
+ },
126
+ fields: [],
127
+ }
128
+ ```
129
+
130
+ ### 2. Manual Permission Verification (`hasPermission`)
131
+
132
+ For custom endpoints, hooks, or conditionally rendering logic, use the `hasPermission` utility:
133
+
134
+ ```typescript
135
+ import { hasPermission } from 'payload-rbac-plugin'
136
+
137
+ const myCustomEndpoint = (req, res) => {
138
+ const user = req.user
139
+
140
+ if (hasPermission(user, 'export:reports')) {
141
+ // allow operation
142
+ } else {
143
+ res.status(403).send('Forbidden')
144
+ }
145
+ }
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Bulk Permission Generator UI
151
+
152
+ When navigating to the **Permissions** collection in your Payload Admin panel:
153
+ 1. Select **Bulk CRUD Generator** in the `Type` select box (visible only during document creation).
154
+ 2. Enter the target collection slug (e.g. `posts`) in the `Collection Name` field.
155
+ 3. Check the CRUD operations you wish to generate (e.g., `create`, `read`).
156
+ 4. Save the document.
157
+ 5. The plugin runs a `beforeChange` hook that creates individual flat records (`posts:create`, `posts:read`) in the database, and avoids saving any temporary helper fields to the database.
158
+
159
+ ---
160
+
161
+ ## Development & Testing
162
+
163
+ If you are developing this plugin locally, you can run the test suite:
164
+
165
+ ```bash
166
+ # Run unit and integration tests
167
+ pnpm test:int
168
+
169
+ # Run tests in watch mode
170
+ pnpm test:int --watch
171
+ ```
172
+
173
+ ---
174
+
175
+ ## License
176
+
177
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ import type { PluginOptions } from '../types';
3
+ export declare const createPermissionsCollection: (options: PluginOptions) => CollectionConfig;
@@ -0,0 +1,121 @@
1
+ import { createBeforePermissionChangeHook } from '../hooks/beforePermissionChange';
2
+ export const createPermissionsCollection = (options)=>{
3
+ const slug = options.permissionsCollectionSlug || 'permissions';
4
+ const customFields = options.permissionsFields || [];
5
+ return {
6
+ slug,
7
+ access: {
8
+ read: ()=>true
9
+ },
10
+ admin: {
11
+ group: 'Access Control',
12
+ hidden: options.hidePermissions ?? false,
13
+ useAsTitle: 'name'
14
+ },
15
+ fields: [
16
+ {
17
+ name: 'type',
18
+ type: 'select',
19
+ admin: {
20
+ condition: (data)=>!data?.id && !data?._id && !data?.createdAt,
21
+ description: 'Choose whether to create a single custom permission or generate multiple CRUD permissions at once.',
22
+ disableListColumn: true,
23
+ disableListFilter: true
24
+ },
25
+ defaultValue: 'single',
26
+ options: [
27
+ {
28
+ label: 'Single Permission',
29
+ value: 'single'
30
+ },
31
+ {
32
+ label: 'Bulk CRUD Generator',
33
+ value: 'bulk'
34
+ }
35
+ ]
36
+ },
37
+ {
38
+ name: 'name',
39
+ type: 'text',
40
+ admin: {
41
+ condition: (data)=>data?.type === 'single',
42
+ description: 'The unique name of the permission (e.g., "access:admin", "posts:create").'
43
+ },
44
+ unique: true,
45
+ validate: (value, { data })=>{
46
+ if (data?.type === 'single' && !value) {
47
+ return 'Name is required for single permissions.';
48
+ }
49
+ return true;
50
+ }
51
+ },
52
+ {
53
+ name: 'collectionName',
54
+ type: 'text',
55
+ admin: {
56
+ condition: (data)=>data?.type === 'bulk',
57
+ description: 'Enter a collection slug (e.g., "posts") to auto-generate permissions.',
58
+ disableListColumn: true,
59
+ disableListFilter: true
60
+ }
61
+ },
62
+ {
63
+ type: 'row',
64
+ fields: [
65
+ {
66
+ name: 'create',
67
+ type: 'checkbox',
68
+ admin: {
69
+ condition: (data)=>data?.type === 'bulk',
70
+ disableListColumn: true,
71
+ disableListFilter: true,
72
+ width: '25%'
73
+ },
74
+ defaultValue: false
75
+ },
76
+ {
77
+ name: 'read',
78
+ type: 'checkbox',
79
+ admin: {
80
+ condition: (data)=>data?.type === 'bulk',
81
+ disableListColumn: true,
82
+ disableListFilter: true,
83
+ width: '25%'
84
+ },
85
+ defaultValue: false
86
+ },
87
+ {
88
+ name: 'update',
89
+ type: 'checkbox',
90
+ admin: {
91
+ condition: (data)=>data?.type === 'bulk',
92
+ disableListColumn: true,
93
+ disableListFilter: true,
94
+ width: '25%'
95
+ },
96
+ defaultValue: false
97
+ },
98
+ {
99
+ name: 'delete',
100
+ type: 'checkbox',
101
+ admin: {
102
+ condition: (data)=>data?.type === 'bulk',
103
+ disableListColumn: true,
104
+ disableListFilter: true,
105
+ width: '25%'
106
+ },
107
+ defaultValue: false
108
+ }
109
+ ]
110
+ },
111
+ ...customFields
112
+ ],
113
+ hooks: {
114
+ beforeChange: [
115
+ createBeforePermissionChangeHook(slug)
116
+ ]
117
+ }
118
+ };
119
+ };
120
+
121
+ //# sourceMappingURL=Permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/collections/Permissions.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nimport type { PluginOptions } from '../types'\n\nimport { createBeforePermissionChangeHook } from '../hooks/beforePermissionChange'\n\nexport const createPermissionsCollection = (options: PluginOptions): CollectionConfig => {\n const slug = options.permissionsCollectionSlug || 'permissions'\n const customFields = options.permissionsFields || []\n\n return {\n slug,\n access: {\n read: () => true,\n },\n admin: {\n group: 'Access Control',\n hidden: options.hidePermissions ?? false,\n useAsTitle: 'name',\n },\n fields: [\n {\n name: 'type',\n type: 'select',\n admin: {\n condition: (data) => !data?.id && !data?._id && !data?.createdAt,\n description:\n 'Choose whether to create a single custom permission or generate multiple CRUD permissions at once.',\n disableListColumn: true,\n disableListFilter: true,\n },\n defaultValue: 'single',\n options: [\n { label: 'Single Permission', value: 'single' },\n { label: 'Bulk CRUD Generator', value: 'bulk' },\n ],\n },\n {\n name: 'name',\n type: 'text',\n admin: {\n condition: (data) => data?.type === 'single',\n description: 'The unique name of the permission (e.g., \"access:admin\", \"posts:create\").',\n },\n unique: true,\n validate: (value: null | string | undefined, { data }: { data?: any }) => {\n if (data?.type === 'single' && !value) {\n return 'Name is required for single permissions.'\n }\n return true\n },\n },\n {\n name: 'collectionName',\n type: 'text',\n admin: {\n condition: (data) => data?.type === 'bulk',\n description: 'Enter a collection slug (e.g., \"posts\") to auto-generate permissions.',\n disableListColumn: true,\n disableListFilter: true,\n },\n },\n {\n type: 'row',\n fields: [\n {\n name: 'create',\n type: 'checkbox',\n admin: {\n condition: (data) => data?.type === 'bulk',\n disableListColumn: true,\n disableListFilter: true,\n width: '25%',\n },\n defaultValue: false,\n },\n {\n name: 'read',\n type: 'checkbox',\n admin: {\n condition: (data) => data?.type === 'bulk',\n disableListColumn: true,\n disableListFilter: true,\n width: '25%',\n },\n defaultValue: false,\n },\n {\n name: 'update',\n type: 'checkbox',\n admin: {\n condition: (data) => data?.type === 'bulk',\n disableListColumn: true,\n disableListFilter: true,\n width: '25%',\n },\n defaultValue: false,\n },\n {\n name: 'delete',\n type: 'checkbox',\n admin: {\n condition: (data) => data?.type === 'bulk',\n disableListColumn: true,\n disableListFilter: true,\n width: '25%',\n },\n defaultValue: false,\n },\n ],\n },\n ...customFields,\n ],\n hooks: {\n beforeChange: [createBeforePermissionChangeHook(slug)],\n },\n }\n}\n"],"names":["createBeforePermissionChangeHook","createPermissionsCollection","options","slug","permissionsCollectionSlug","customFields","permissionsFields","access","read","admin","group","hidden","hidePermissions","useAsTitle","fields","name","type","condition","data","id","_id","createdAt","description","disableListColumn","disableListFilter","defaultValue","label","value","unique","validate","width","hooks","beforeChange"],"mappings":"AAIA,SAASA,gCAAgC,QAAQ,kCAAiC;AAElF,OAAO,MAAMC,8BAA8B,CAACC;IAC1C,MAAMC,OAAOD,QAAQE,yBAAyB,IAAI;IAClD,MAAMC,eAAeH,QAAQI,iBAAiB,IAAI,EAAE;IAEpD,OAAO;QACLH;QACAI,QAAQ;YACNC,MAAM,IAAM;QACd;QACAC,OAAO;YACLC,OAAO;YACPC,QAAQT,QAAQU,eAAe,IAAI;YACnCC,YAAY;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLQ,WAAW,CAACC,OAAS,CAACA,MAAMC,MAAM,CAACD,MAAME,OAAO,CAACF,MAAMG;oBACvDC,aACE;oBACFC,mBAAmB;oBACnBC,mBAAmB;gBACrB;gBACAC,cAAc;gBACdvB,SAAS;oBACP;wBAAEwB,OAAO;wBAAqBC,OAAO;oBAAS;oBAC9C;wBAAED,OAAO;wBAAuBC,OAAO;oBAAO;iBAC/C;YACH;YACA;gBACEZ,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;oBACpCM,aAAa;gBACf;gBACAM,QAAQ;gBACRC,UAAU,CAACF,OAAkC,EAAET,IAAI,EAAkB;oBACnE,IAAIA,MAAMF,SAAS,YAAY,CAACW,OAAO;wBACrC,OAAO;oBACT;oBACA,OAAO;gBACT;YACF;YACA;gBACEZ,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;oBACpCM,aAAa;oBACbC,mBAAmB;oBACnBC,mBAAmB;gBACrB;YACF;YACA;gBACER,MAAM;gBACNF,QAAQ;oBACN;wBACEC,MAAM;wBACNC,MAAM;wBACNP,OAAO;4BACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;4BACpCO,mBAAmB;4BACnBC,mBAAmB;4BACnBM,OAAO;wBACT;wBACAL,cAAc;oBAChB;oBACA;wBACEV,MAAM;wBACNC,MAAM;wBACNP,OAAO;4BACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;4BACpCO,mBAAmB;4BACnBC,mBAAmB;4BACnBM,OAAO;wBACT;wBACAL,cAAc;oBAChB;oBACA;wBACEV,MAAM;wBACNC,MAAM;wBACNP,OAAO;4BACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;4BACpCO,mBAAmB;4BACnBC,mBAAmB;4BACnBM,OAAO;wBACT;wBACAL,cAAc;oBAChB;oBACA;wBACEV,MAAM;wBACNC,MAAM;wBACNP,OAAO;4BACLQ,WAAW,CAACC,OAASA,MAAMF,SAAS;4BACpCO,mBAAmB;4BACnBC,mBAAmB;4BACnBM,OAAO;wBACT;wBACAL,cAAc;oBAChB;iBACD;YACH;eACGpB;SACJ;QACD0B,OAAO;YACLC,cAAc;gBAAChC,iCAAiCG;aAAM;QACxD;IACF;AACF,EAAC"}
@@ -0,0 +1,3 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ import type { PluginOptions } from '../types';
3
+ export declare const createRolesCollection: (options: PluginOptions) => CollectionConfig;
@@ -0,0 +1,39 @@
1
+ export const createRolesCollection = (options)=>{
2
+ const slug = options.rolesCollectionSlug || 'roles';
3
+ const permissionsSlug = options.permissionsCollectionSlug || 'permissions';
4
+ const customFields = options.rolesFields || [];
5
+ return {
6
+ slug,
7
+ admin: {
8
+ useAsTitle: 'name',
9
+ group: 'Access Control',
10
+ hidden: options.hideRoles ?? false
11
+ },
12
+ access: {
13
+ read: ()=>true
14
+ },
15
+ fields: [
16
+ {
17
+ name: 'name',
18
+ type: 'text',
19
+ required: true,
20
+ unique: true,
21
+ admin: {
22
+ description: 'The unique name of the role (e.g., "admin", "editor").'
23
+ }
24
+ },
25
+ {
26
+ name: 'permissions',
27
+ type: 'relationship',
28
+ relationTo: permissionsSlug,
29
+ hasMany: true,
30
+ admin: {
31
+ description: 'The permissions assigned to this role.'
32
+ }
33
+ },
34
+ ...customFields
35
+ ]
36
+ };
37
+ };
38
+
39
+ //# sourceMappingURL=Roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/collections/Roles.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload';\nimport type { PluginOptions } from '../types';\n\nexport const createRolesCollection = (options: PluginOptions): CollectionConfig => {\n const slug = options.rolesCollectionSlug || 'roles'\n const permissionsSlug = options.permissionsCollectionSlug || 'permissions'\n const customFields = options.rolesFields || []\n\n return {\n slug,\n admin: {\n useAsTitle: 'name',\n group: 'Access Control',\n hidden: options.hideRoles ?? false,\n },\n access: {\n read: () => true,\n },\n fields: [\n {\n name: 'name',\n type: 'text',\n required: true,\n unique: true,\n admin: {\n description: 'The unique name of the role (e.g., \"admin\", \"editor\").',\n },\n },\n {\n name: 'permissions',\n type: 'relationship',\n relationTo: permissionsSlug,\n hasMany: true,\n admin: {\n description: 'The permissions assigned to this role.',\n },\n },\n ...customFields,\n ],\n }\n}\n"],"names":["createRolesCollection","options","slug","rolesCollectionSlug","permissionsSlug","permissionsCollectionSlug","customFields","rolesFields","admin","useAsTitle","group","hidden","hideRoles","access","read","fields","name","type","required","unique","description","relationTo","hasMany"],"mappings":"AAGA,OAAO,MAAMA,wBAAwB,CAACC;IACpC,MAAMC,OAAOD,QAAQE,mBAAmB,IAAI;IAC5C,MAAMC,kBAAkBH,QAAQI,yBAAyB,IAAI;IAC7D,MAAMC,eAAeL,QAAQM,WAAW,IAAI,EAAE;IAE9C,OAAO;QACLL;QACAM,OAAO;YACLC,YAAY;YACZC,OAAO;YACPC,QAAQV,QAAQW,SAAS,IAAI;QAC/B;QACAC,QAAQ;YACNC,MAAM,IAAM;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNC,UAAU;gBACVC,QAAQ;gBACRX,OAAO;oBACLY,aAAa;gBACf;YACF;YACA;gBACEJ,MAAM;gBACNC,MAAM;gBACNI,YAAYjB;gBACZkB,SAAS;gBACTd,OAAO;oBACLY,aAAa;gBACf;YACF;eACGd;SACJ;IACH;AACF,EAAC"}
@@ -0,0 +1 @@
1
+ export declare const BeforeDashboardClient: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useConfig } from '@payloadcms/ui';
4
+ import { formatAdminURL } from 'payload/shared';
5
+ import { useEffect, useState } from 'react';
6
+ export const BeforeDashboardClient = ()=>{
7
+ const { config } = useConfig();
8
+ const [message, setMessage] = useState('');
9
+ useEffect(()=>{
10
+ const fetchMessage = async ()=>{
11
+ const response = await fetch(formatAdminURL({
12
+ apiRoute: config.routes.api,
13
+ path: '/my-plugin-endpoint'
14
+ }));
15
+ const result = await response.json();
16
+ setMessage(result.message);
17
+ };
18
+ void fetchMessage();
19
+ }, [
20
+ config.serverURL,
21
+ config.routes.api
22
+ ]);
23
+ return /*#__PURE__*/ _jsxs("div", {
24
+ children: [
25
+ /*#__PURE__*/ _jsx("h1", {
26
+ children: "Added by the plugin: Before Dashboard Client"
27
+ }),
28
+ /*#__PURE__*/ _jsxs("div", {
29
+ children: [
30
+ "Message from the endpoint:",
31
+ /*#__PURE__*/ _jsx("div", {
32
+ children: message || 'Loading...'
33
+ })
34
+ ]
35
+ })
36
+ ]
37
+ });
38
+ };
39
+
40
+ //# sourceMappingURL=BeforeDashboardClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/BeforeDashboardClient.tsx"],"sourcesContent":["'use client'\nimport { useConfig } from '@payloadcms/ui'\nimport { formatAdminURL } from 'payload/shared'\nimport { useEffect, useState } from 'react'\n\nexport const BeforeDashboardClient = () => {\n const { config } = useConfig()\n\n const [message, setMessage] = useState('')\n\n useEffect(() => {\n const fetchMessage = async () => {\n const response = await fetch(\n formatAdminURL({\n apiRoute: config.routes.api,\n path: '/my-plugin-endpoint',\n }),\n )\n const result = await response.json()\n setMessage(result.message)\n }\n\n void fetchMessage()\n }, [config.serverURL, config.routes.api])\n\n return (\n <div>\n <h1>Added by the plugin: Before Dashboard Client</h1>\n <div>\n Message from the endpoint:\n <div>{message || 'Loading...'}</div>\n </div>\n </div>\n )\n}\n"],"names":["useConfig","formatAdminURL","useEffect","useState","BeforeDashboardClient","config","message","setMessage","fetchMessage","response","fetch","apiRoute","routes","api","path","result","json","serverURL","div","h1"],"mappings":"AAAA;;AACA,SAASA,SAAS,QAAQ,iBAAgB;AAC1C,SAASC,cAAc,QAAQ,iBAAgB;AAC/C,SAASC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAE3C,OAAO,MAAMC,wBAAwB;IACnC,MAAM,EAAEC,MAAM,EAAE,GAAGL;IAEnB,MAAM,CAACM,SAASC,WAAW,GAAGJ,SAAS;IAEvCD,UAAU;QACR,MAAMM,eAAe;YACnB,MAAMC,WAAW,MAAMC,MACrBT,eAAe;gBACbU,UAAUN,OAAOO,MAAM,CAACC,GAAG;gBAC3BC,MAAM;YACR;YAEF,MAAMC,SAAS,MAAMN,SAASO,IAAI;YAClCT,WAAWQ,OAAOT,OAAO;QAC3B;QAEA,KAAKE;IACP,GAAG;QAACH,OAAOY,SAAS;QAAEZ,OAAOO,MAAM,CAACC,GAAG;KAAC;IAExC,qBACE,MAACK;;0BACC,KAACC;0BAAG;;0BACJ,MAACD;;oBAAI;kCAEH,KAACA;kCAAKZ,WAAW;;;;;;AAIzB,EAAC"}
@@ -0,0 +1,2 @@
1
+ import type { ServerComponentProps } from 'payload';
2
+ export declare const BeforeDashboardServer: (props: ServerComponentProps) => Promise<import("react/jsx-runtime").JSX.Element>;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import styles from './BeforeDashboardServer.module.css';
3
+ export const BeforeDashboardServer = async (props)=>{
4
+ const { payload } = props;
5
+ const { docs } = await payload.find({
6
+ collection: 'plugin-collection'
7
+ });
8
+ return /*#__PURE__*/ _jsxs("div", {
9
+ className: styles.wrapper,
10
+ children: [
11
+ /*#__PURE__*/ _jsx("h1", {
12
+ children: "Added by the plugin: Before Dashboard Server"
13
+ }),
14
+ "Docs from Local API:",
15
+ docs.map((doc)=>/*#__PURE__*/ _jsx("div", {
16
+ children: doc.id
17
+ }, doc.id))
18
+ ]
19
+ });
20
+ };
21
+
22
+ //# sourceMappingURL=BeforeDashboardServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/BeforeDashboardServer.tsx"],"sourcesContent":["import type { ServerComponentProps } from 'payload'\n\nimport styles from './BeforeDashboardServer.module.css'\n\nexport const BeforeDashboardServer = async (props: ServerComponentProps) => {\n const { payload } = props\n\n const { docs } = await payload.find({ collection: 'plugin-collection' })\n\n return (\n <div className={styles.wrapper}>\n <h1>Added by the plugin: Before Dashboard Server</h1>\n Docs from Local API:\n {docs.map((doc) => (\n <div key={doc.id}>{doc.id}</div>\n ))}\n </div>\n )\n}\n"],"names":["styles","BeforeDashboardServer","props","payload","docs","find","collection","div","className","wrapper","h1","map","doc","id"],"mappings":";AAEA,OAAOA,YAAY,qCAAoC;AAEvD,OAAO,MAAMC,wBAAwB,OAAOC;IAC1C,MAAM,EAAEC,OAAO,EAAE,GAAGD;IAEpB,MAAM,EAAEE,IAAI,EAAE,GAAG,MAAMD,QAAQE,IAAI,CAAC;QAAEC,YAAY;IAAoB;IAEtE,qBACE,MAACC;QAAIC,WAAWR,OAAOS,OAAO;;0BAC5B,KAACC;0BAAG;;YAAiD;YAEpDN,KAAKO,GAAG,CAAC,CAACC,oBACT,KAACL;8BAAkBK,IAAIC,EAAE;mBAAfD,IAAIC,EAAE;;;AAIxB,EAAC"}
@@ -0,0 +1,5 @@
1
+ .wrapper {
2
+ display: flex;
3
+ gap: 5px;
4
+ flex-direction: column;
5
+ }
@@ -0,0 +1,2 @@
1
+ import type { PayloadHandler } from 'payload';
2
+ export declare const customEndpointHandler: PayloadHandler;
@@ -0,0 +1,7 @@
1
+ export const customEndpointHandler = ()=>{
2
+ return Response.json({
3
+ message: 'Hello from custom endpoint'
4
+ });
5
+ };
6
+
7
+ //# sourceMappingURL=customEndpointHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/customEndpointHandler.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\n\nexport const customEndpointHandler: PayloadHandler = () => {\n return Response.json({ message: 'Hello from custom endpoint' })\n}\n"],"names":["customEndpointHandler","Response","json","message"],"mappings":"AAEA,OAAO,MAAMA,wBAAwC;IACnD,OAAOC,SAASC,IAAI,CAAC;QAAEC,SAAS;IAA6B;AAC/D,EAAC"}
@@ -0,0 +1 @@
1
+ export { BeforeDashboardClient } from '../components/BeforeDashboardClient';
@@ -0,0 +1,3 @@
1
+ export { BeforeDashboardClient } from '../components/BeforeDashboardClient';
2
+
3
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { BeforeDashboardClient } from '../components/BeforeDashboardClient';\n\n"],"names":["BeforeDashboardClient"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,sCAAsC"}
@@ -0,0 +1 @@
1
+ export { BeforeDashboardServer } from '../components/BeforeDashboardServer';
@@ -0,0 +1,3 @@
1
+ export { BeforeDashboardServer } from '../components/BeforeDashboardServer';
2
+
3
+ //# sourceMappingURL=rsc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/exports/rsc.ts"],"sourcesContent":["export { BeforeDashboardServer } from '../components/BeforeDashboardServer';\n\n"],"names":["BeforeDashboardServer"],"mappings":"AAAA,SAASA,qBAAqB,QAAQ,sCAAsC"}
@@ -0,0 +1,2 @@
1
+ import type { CollectionBeforeChangeHook } from 'payload';
2
+ export declare const createBeforePermissionChangeHook: (slug: string) => CollectionBeforeChangeHook;
@@ -0,0 +1,58 @@
1
+ export const createBeforePermissionChangeHook = (slug)=>{
2
+ return async ({ data, req })=>{
3
+ // Only run generator logic on create/update if type is bulk
4
+ if (data.type === 'bulk' && data.collectionName) {
5
+ const actions = [
6
+ 'create',
7
+ 'read',
8
+ 'update',
9
+ 'delete'
10
+ ].filter((action)=>data[action] === true);
11
+ if (actions.length === 0) {
12
+ throw new Error('You must select at least one CRUD operation when using the Bulk Generator.');
13
+ }
14
+ // e.g. ["posts:create", "posts:read"]
15
+ const generatedNames = actions.map((action)=>`${data.collectionName}:${action}`);
16
+ // Filter out names that already exist
17
+ const uniqueNames = [];
18
+ for (const name of generatedNames){
19
+ const existing = await req.payload.find({
20
+ collection: slug,
21
+ where: {
22
+ name: {
23
+ equals: name
24
+ }
25
+ }
26
+ });
27
+ if (existing.docs.length === 0) {
28
+ uniqueNames.push(name);
29
+ }
30
+ }
31
+ if (uniqueNames.length === 0) {
32
+ throw new Error('All selected CRUD permissions already exist.');
33
+ }
34
+ // Mutate current document to save the first unique generated name
35
+ data.name = uniqueNames[0];
36
+ // Create the remaining unique permissions in the background
37
+ const remainingNames = uniqueNames.slice(1);
38
+ for (const name of remainingNames){
39
+ await req.payload.create({
40
+ collection: slug,
41
+ data: {
42
+ name
43
+ }
44
+ });
45
+ }
46
+ }
47
+ // Ensure UI-only fields are never saved to the database
48
+ delete data.type;
49
+ delete data.collectionName;
50
+ delete data.create;
51
+ delete data.read;
52
+ delete data.update;
53
+ delete data.delete;
54
+ return data;
55
+ };
56
+ };
57
+
58
+ //# sourceMappingURL=beforePermissionChange.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/beforePermissionChange.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nexport const createBeforePermissionChangeHook = (slug: string): CollectionBeforeChangeHook => {\n return async ({ data, req }) => {\n // Only run generator logic on create/update if type is bulk\n if (data.type === 'bulk' && data.collectionName) {\n const actions = ['create', 'read', 'update', 'delete'].filter(\n (action) => data[action] === true,\n )\n\n if (actions.length === 0) {\n throw new Error('You must select at least one CRUD operation when using the Bulk Generator.')\n }\n\n // e.g. [\"posts:create\", \"posts:read\"]\n const generatedNames = actions.map((action) => `${data.collectionName}:${action}`)\n\n // Filter out names that already exist\n const uniqueNames: string[] = []\n for (const name of generatedNames) {\n const existing = await req.payload.find({\n collection: slug,\n where: {\n name: { equals: name },\n },\n })\n if (existing.docs.length === 0) {\n uniqueNames.push(name)\n }\n }\n\n if (uniqueNames.length === 0) {\n throw new Error('All selected CRUD permissions already exist.')\n }\n\n // Mutate current document to save the first unique generated name\n data.name = uniqueNames[0]\n\n // Create the remaining unique permissions in the background\n const remainingNames = uniqueNames.slice(1)\n for (const name of remainingNames) {\n await req.payload.create({\n collection: slug,\n data: {\n name,\n },\n })\n }\n }\n\n // Ensure UI-only fields are never saved to the database\n delete data.type\n delete data.collectionName\n delete data.create\n delete data.read\n delete data.update\n delete data.delete\n\n return data\n }\n}\n"],"names":["createBeforePermissionChangeHook","slug","data","req","type","collectionName","actions","filter","action","length","Error","generatedNames","map","uniqueNames","name","existing","payload","find","collection","where","equals","docs","push","remainingNames","slice","create","read","update","delete"],"mappings":"AAEA,OAAO,MAAMA,mCAAmC,CAACC;IAC/C,OAAO,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAE;QACzB,4DAA4D;QAC5D,IAAID,KAAKE,IAAI,KAAK,UAAUF,KAAKG,cAAc,EAAE;YAC/C,MAAMC,UAAU;gBAAC;gBAAU;gBAAQ;gBAAU;aAAS,CAACC,MAAM,CAC3D,CAACC,SAAWN,IAAI,CAACM,OAAO,KAAK;YAG/B,IAAIF,QAAQG,MAAM,KAAK,GAAG;gBACxB,MAAM,IAAIC,MAAM;YAClB;YAEA,sCAAsC;YACtC,MAAMC,iBAAiBL,QAAQM,GAAG,CAAC,CAACJ,SAAW,GAAGN,KAAKG,cAAc,CAAC,CAAC,EAAEG,QAAQ;YAEjF,sCAAsC;YACtC,MAAMK,cAAwB,EAAE;YAChC,KAAK,MAAMC,QAAQH,eAAgB;gBACjC,MAAMI,WAAW,MAAMZ,IAAIa,OAAO,CAACC,IAAI,CAAC;oBACtCC,YAAYjB;oBACZkB,OAAO;wBACLL,MAAM;4BAAEM,QAAQN;wBAAK;oBACvB;gBACF;gBACA,IAAIC,SAASM,IAAI,CAACZ,MAAM,KAAK,GAAG;oBAC9BI,YAAYS,IAAI,CAACR;gBACnB;YACF;YAEA,IAAID,YAAYJ,MAAM,KAAK,GAAG;gBAC5B,MAAM,IAAIC,MAAM;YAClB;YAEA,kEAAkE;YAClER,KAAKY,IAAI,GAAGD,WAAW,CAAC,EAAE;YAE1B,4DAA4D;YAC5D,MAAMU,iBAAiBV,YAAYW,KAAK,CAAC;YACzC,KAAK,MAAMV,QAAQS,eAAgB;gBACjC,MAAMpB,IAAIa,OAAO,CAACS,MAAM,CAAC;oBACvBP,YAAYjB;oBACZC,MAAM;wBACJY;oBACF;gBACF;YACF;QACF;QAEA,wDAAwD;QACxD,OAAOZ,KAAKE,IAAI;QAChB,OAAOF,KAAKG,cAAc;QAC1B,OAAOH,KAAKuB,MAAM;QAClB,OAAOvB,KAAKwB,IAAI;QAChB,OAAOxB,KAAKyB,MAAM;QAClB,OAAOzB,KAAK0B,MAAM;QAElB,OAAO1B;IACT;AACF,EAAC"}
@@ -0,0 +1,5 @@
1
+ import type { Config } from 'payload';
2
+ import type { PluginOptions } from './types';
3
+ export declare const rbac: (pluginOptions?: PluginOptions) => (config: Config) => Config;
4
+ export * from './types';
5
+ export * from './utilities/index';
package/dist/index.js ADDED
@@ -0,0 +1,38 @@
1
+ import { createPermissionsCollection } from './collections/Permissions';
2
+ import { createRolesCollection } from './collections/Roles';
3
+ export const rbac = (pluginOptions)=>(config)=>{
4
+ const options = pluginOptions || {};
5
+ if (options.enabled === false) {
6
+ return config;
7
+ }
8
+ if (!config.collections) {
9
+ config.collections = [];
10
+ }
11
+ // Add Roles and Permissions collections
12
+ config.collections.push(createPermissionsCollection(options));
13
+ config.collections.push(createRolesCollection(options));
14
+ // Extend the target auth collection
15
+ const authSlug = options.authCollectionSlug || 'users';
16
+ const rolesSlug = options.rolesCollectionSlug || 'roles';
17
+ const authCollection = config.collections.find((collection)=>collection.slug === authSlug);
18
+ if (authCollection) {
19
+ authCollection.fields.push({
20
+ name: 'roles',
21
+ type: 'relationship',
22
+ relationTo: rolesSlug,
23
+ hasMany: true,
24
+ saveToJWT: true,
25
+ admin: {
26
+ description: 'Roles assigned to this user.'
27
+ }
28
+ });
29
+ } else {
30
+ console.warn(`[rbac-plugin] Auth collection '${authSlug}' not found. Cannot inject roles field.`);
31
+ }
32
+ return config;
33
+ };
34
+ // Export utilities and types for consumers
35
+ export * from './types';
36
+ export * from './utilities/index';
37
+
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload';\nimport type { PluginOptions } from './types';\n\nimport { createPermissionsCollection } from './collections/Permissions';\nimport { createRolesCollection } from './collections/Roles';\n\nexport const rbac =\n (pluginOptions?: PluginOptions) =>\n (config: Config): Config => {\n const options = pluginOptions || {}\n \n if (options.enabled === false) {\n return config\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n // Add Roles and Permissions collections\n config.collections.push(createPermissionsCollection(options))\n config.collections.push(createRolesCollection(options))\n\n // Extend the target auth collection\n const authSlug = options.authCollectionSlug || 'users'\n const rolesSlug = options.rolesCollectionSlug || 'roles'\n\n const authCollection = config.collections.find(\n (collection) => collection.slug === authSlug,\n )\n\n if (authCollection) {\n authCollection.fields.push({\n name: 'roles',\n type: 'relationship',\n relationTo: rolesSlug,\n hasMany: true,\n saveToJWT: true,\n admin: {\n description: 'Roles assigned to this user.',\n },\n })\n } else {\n console.warn(`[rbac-plugin] Auth collection '${authSlug}' not found. Cannot inject roles field.`)\n }\n\n return config\n }\n\n// Export utilities and types for consumers\nexport * from './types';\nexport * from './utilities/index';\n\n"],"names":["createPermissionsCollection","createRolesCollection","rbac","pluginOptions","config","options","enabled","collections","push","authSlug","authCollectionSlug","rolesSlug","rolesCollectionSlug","authCollection","find","collection","slug","fields","name","type","relationTo","hasMany","saveToJWT","admin","description","console","warn"],"mappings":"AAGA,SAASA,2BAA2B,QAAQ,4BAA4B;AACxE,SAASC,qBAAqB,QAAQ,sBAAsB;AAE5D,OAAO,MAAMC,OACX,CAACC,gBACD,CAACC;QACC,MAAMC,UAAUF,iBAAiB,CAAC;QAElC,IAAIE,QAAQC,OAAO,KAAK,OAAO;YAC7B,OAAOF;QACT;QAEA,IAAI,CAACA,OAAOG,WAAW,EAAE;YACvBH,OAAOG,WAAW,GAAG,EAAE;QACzB;QAEA,wCAAwC;QACxCH,OAAOG,WAAW,CAACC,IAAI,CAACR,4BAA4BK;QACpDD,OAAOG,WAAW,CAACC,IAAI,CAACP,sBAAsBI;QAE9C,oCAAoC;QACpC,MAAMI,WAAWJ,QAAQK,kBAAkB,IAAI;QAC/C,MAAMC,YAAYN,QAAQO,mBAAmB,IAAI;QAEjD,MAAMC,iBAAiBT,OAAOG,WAAW,CAACO,IAAI,CAC5C,CAACC,aAAeA,WAAWC,IAAI,KAAKP;QAGtC,IAAII,gBAAgB;YAClBA,eAAeI,MAAM,CAACT,IAAI,CAAC;gBACzBU,MAAM;gBACNC,MAAM;gBACNC,YAAYT;gBACZU,SAAS;gBACTC,WAAW;gBACXC,OAAO;oBACLC,aAAa;gBACf;YACF;QACF,OAAO;YACLC,QAAQC,IAAI,CAAC,CAAC,+BAA+B,EAAEjB,SAAS,uCAAuC,CAAC;QAClG;QAEA,OAAOL;IACT,EAAC;AAEH,2CAA2C;AAC3C,cAAc,UAAU;AACxB,cAAc,oBAAoB"}
@@ -0,0 +1,47 @@
1
+ import type { Field } from 'payload';
2
+ export interface PluginOptions {
3
+ /**
4
+ * Enable or disable plugin
5
+ * @default false
6
+ */
7
+ enabled?: boolean;
8
+ /**
9
+ * Override the default collection slug for Roles
10
+ * @default 'roles'
11
+ */
12
+ rolesCollectionSlug?: string;
13
+ /**
14
+ * Override the default collection slug for Permissions
15
+ * @default 'permissions'
16
+ */
17
+ permissionsCollectionSlug?: string;
18
+ /**
19
+ * The collection slug for the authentication collection (typically 'users')
20
+ * @default 'users'
21
+ */
22
+ authCollectionSlug?: string;
23
+ /**
24
+ * Inject additional custom fields into the generated Roles collection
25
+ */
26
+ rolesFields?: Field[];
27
+ /**
28
+ * Inject additional custom fields into the generated Permissions collection
29
+ */
30
+ permissionsFields?: Field[];
31
+ /**
32
+ * Hide the Roles collection from the Admin panel sidebar/dashboard
33
+ * Can be a boolean or a function based on the logged-in user
34
+ * @default false
35
+ */
36
+ hideRoles?: boolean | ((args: {
37
+ user: any;
38
+ }) => boolean);
39
+ /**
40
+ * Hide the Permissions collection from the Admin panel sidebar/dashboard
41
+ * Can be a boolean or a function based on the logged-in user
42
+ * @default false
43
+ */
44
+ hidePermissions?: boolean | ((args: {
45
+ user: any;
46
+ }) => boolean);
47
+ }
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ export { };
2
+
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nexport interface PluginOptions {\n /**\n * Enable or disable plugin\n * @default false\n */\n enabled?: boolean\n\n /**\n * Override the default collection slug for Roles\n * @default 'roles'\n */\n rolesCollectionSlug?: string\n\n /**\n * Override the default collection slug for Permissions\n * @default 'permissions'\n */\n permissionsCollectionSlug?: string\n\n /**\n * The collection slug for the authentication collection (typically 'users')\n * @default 'users'\n */\n authCollectionSlug?: string\n\n /**\n * Inject additional custom fields into the generated Roles collection\n */\n rolesFields?: Field[]\n\n /**\n * Inject additional custom fields into the generated Permissions collection\n */\n permissionsFields?: Field[]\n\n /**\n * Hide the Roles collection from the Admin panel sidebar/dashboard\n * Can be a boolean or a function based on the logged-in user\n * @default false\n */\n hideRoles?: boolean | ((args: { user: any }) => boolean)\n\n /**\n * Hide the Permissions collection from the Admin panel sidebar/dashboard\n * Can be a boolean or a function based on the logged-in user\n * @default false\n */\n hidePermissions?: boolean | ((args: { user: any }) => boolean)\n}\n"],"names":[],"mappings":"AAEA,WAgDC"}
@@ -0,0 +1,8 @@
1
+ import type { Access } from 'payload';
2
+ /**
3
+ * A Higher-Order Function to drop into Payload Collection access control fields.
4
+ *
5
+ * @param permissionName - The required permission name
6
+ * @returns Access function
7
+ */
8
+ export declare const checkPermission: (permissionName: string) => Access;
@@ -0,0 +1,16 @@
1
+ import { hasPermission } from './hasPermission';
2
+ /**
3
+ * A Higher-Order Function to drop into Payload Collection access control fields.
4
+ *
5
+ * @param permissionName - The required permission name
6
+ * @returns Access function
7
+ */ export const checkPermission = (permissionName)=>{
8
+ return ({ req: { user } })=>{
9
+ // We can just rely on the synchronous hasPermission check.
10
+ // If relations are unpopulated, it will return false. Ensure your auth
11
+ // collection is configured with adequate depth for saveToJWT or default depth.
12
+ return hasPermission(user, permissionName);
13
+ };
14
+ };
15
+
16
+ //# sourceMappingURL=checkPermission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/checkPermission.ts"],"sourcesContent":["import type { Access } from 'payload';\nimport { hasPermission } from './hasPermission';\n\n/**\n * A Higher-Order Function to drop into Payload Collection access control fields.\n * \n * @param permissionName - The required permission name\n * @returns Access function\n */\nexport const checkPermission = (permissionName: string): Access => {\n return ({ req: { user } }) => {\n // We can just rely on the synchronous hasPermission check.\n // If relations are unpopulated, it will return false. Ensure your auth\n // collection is configured with adequate depth for saveToJWT or default depth.\n return hasPermission(user, permissionName)\n }\n}\n"],"names":["hasPermission","checkPermission","permissionName","req","user"],"mappings":"AACA,SAASA,aAAa,QAAQ,kBAAkB;AAEhD;;;;;CAKC,GACD,OAAO,MAAMC,kBAAkB,CAACC;IAC9B,OAAO,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAE,EAAE;QACvB,2DAA2D;QAC3D,uEAAuE;QACvE,+EAA+E;QAC/E,OAAOJ,cAAcI,MAAMF;IAC7B;AACF,EAAC"}
@@ -0,0 +1,10 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ /**
3
+ * Checks if a user has a specific permission.
4
+ * Gracefully handles populated and unpopulated relationship states as far as possible synchronously.
5
+ *
6
+ * @param user - The Payload user object from req.user
7
+ * @param requiredPermission - The name of the permission to check for
8
+ * @returns boolean
9
+ */
10
+ export declare const hasPermission: (user: PayloadRequest["user"] | null | undefined, requiredPermission: string) => boolean;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Checks if a user has a specific permission.
3
+ * Gracefully handles populated and unpopulated relationship states as far as possible synchronously.
4
+ *
5
+ * @param user - The Payload user object from req.user
6
+ * @param requiredPermission - The name of the permission to check for
7
+ * @returns boolean
8
+ */ export const hasPermission = (user, requiredPermission)=>{
9
+ if (!user || !user.roles || !Array.isArray(user.roles)) {
10
+ return false;
11
+ }
12
+ for (const role of user.roles){
13
+ // If role is just an ID (unpopulated), we can't check its permissions synchronously
14
+ if (typeof role !== 'object' || role === null) {
15
+ continue;
16
+ }
17
+ const permissions = role.permissions;
18
+ if (!permissions || !Array.isArray(permissions)) {
19
+ continue;
20
+ }
21
+ for (const permission of permissions){
22
+ if (typeof permission === 'object' && permission !== null) {
23
+ if ('name' in permission && permission.name === requiredPermission) {
24
+ return true;
25
+ }
26
+ }
27
+ }
28
+ }
29
+ return false;
30
+ };
31
+
32
+ //# sourceMappingURL=hasPermission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/hasPermission.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\n/**\n * Checks if a user has a specific permission.\n * Gracefully handles populated and unpopulated relationship states as far as possible synchronously.\n * \n * @param user - The Payload user object from req.user\n * @param requiredPermission - The name of the permission to check for\n * @returns boolean\n */\nexport const hasPermission = (\n user: PayloadRequest['user'] | null | undefined,\n requiredPermission: string,\n): boolean => {\n if (!user || !user.roles || !Array.isArray(user.roles)) {\n return false\n }\n\n for (const role of user.roles) {\n // If role is just an ID (unpopulated), we can't check its permissions synchronously\n if (typeof role !== 'object' || role === null) {\n continue\n }\n\n const permissions = role.permissions\n if (!permissions || !Array.isArray(permissions)) {\n continue\n }\n\n for (const permission of permissions) {\n if (typeof permission === 'object' && permission !== null) {\n if ('name' in permission && permission.name === requiredPermission) {\n return true\n }\n } \n }\n }\n\n return false\n}\n"],"names":["hasPermission","user","requiredPermission","roles","Array","isArray","role","permissions","permission","name"],"mappings":"AAEA;;;;;;;CAOC,GACD,OAAO,MAAMA,gBAAgB,CAC3BC,MACAC;IAEA,IAAI,CAACD,QAAQ,CAACA,KAAKE,KAAK,IAAI,CAACC,MAAMC,OAAO,CAACJ,KAAKE,KAAK,GAAG;QACtD,OAAO;IACT;IAEA,KAAK,MAAMG,QAAQL,KAAKE,KAAK,CAAE;QAC7B,oFAAoF;QACpF,IAAI,OAAOG,SAAS,YAAYA,SAAS,MAAM;YAC7C;QACF;QAEA,MAAMC,cAAcD,KAAKC,WAAW;QACpC,IAAI,CAACA,eAAe,CAACH,MAAMC,OAAO,CAACE,cAAc;YAC/C;QACF;QAEA,KAAK,MAAMC,cAAcD,YAAa;YACpC,IAAI,OAAOC,eAAe,YAAYA,eAAe,MAAM;gBACzD,IAAI,UAAUA,cAAcA,WAAWC,IAAI,KAAKP,oBAAoB;oBAClE,OAAO;gBACT;YACF;QACF;IACF;IAEA,OAAO;AACT,EAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { hasPermission } from './hasPermission';
3
+ describe('hasPermission', ()=>{
4
+ it('should return false if user is null', ()=>{
5
+ expect(hasPermission(null, 'read:posts')).toBe(false);
6
+ });
7
+ it('should return false if user has no roles array', ()=>{
8
+ const user = {
9
+ id: '1'
10
+ };
11
+ expect(hasPermission(user, 'read:posts')).toBe(false);
12
+ });
13
+ it('should return false if user roles is unpopulated (array of strings)', ()=>{
14
+ const user = {
15
+ id: '1',
16
+ roles: [
17
+ 'role-id-1'
18
+ ]
19
+ };
20
+ expect(hasPermission(user, 'read:posts')).toBe(false);
21
+ });
22
+ it('should return false if role has no permissions', ()=>{
23
+ const user = {
24
+ id: '1',
25
+ roles: [
26
+ {
27
+ id: 'role-1',
28
+ name: 'admin'
29
+ }
30
+ ]
31
+ };
32
+ expect(hasPermission(user, 'read:posts')).toBe(false);
33
+ });
34
+ it('should return false if role permissions are unpopulated (array of strings)', ()=>{
35
+ const user = {
36
+ id: '1',
37
+ roles: [
38
+ {
39
+ id: 'role-1',
40
+ name: 'admin',
41
+ permissions: [
42
+ 'perm-id-1'
43
+ ]
44
+ }
45
+ ]
46
+ };
47
+ expect(hasPermission(user, 'read:posts')).toBe(false);
48
+ });
49
+ it('should return true if permission is found in populated roles', ()=>{
50
+ const user = {
51
+ id: '1',
52
+ roles: [
53
+ {
54
+ id: 'role-1',
55
+ name: 'admin',
56
+ permissions: [
57
+ {
58
+ id: 'perm-1',
59
+ name: 'read:posts'
60
+ },
61
+ {
62
+ id: 'perm-2',
63
+ name: 'write:posts'
64
+ }
65
+ ]
66
+ }
67
+ ]
68
+ };
69
+ expect(hasPermission(user, 'write:posts')).toBe(true);
70
+ });
71
+ it('should return false if permission is not found in populated roles', ()=>{
72
+ const user = {
73
+ id: '1',
74
+ roles: [
75
+ {
76
+ id: 'role-1',
77
+ name: 'editor',
78
+ permissions: [
79
+ {
80
+ id: 'perm-1',
81
+ name: 'read:posts'
82
+ }
83
+ ]
84
+ }
85
+ ]
86
+ };
87
+ expect(hasPermission(user, 'delete:posts')).toBe(false);
88
+ });
89
+ it('should correctly search through multiple roles', ()=>{
90
+ const user = {
91
+ id: '1',
92
+ roles: [
93
+ {
94
+ id: 'role-1',
95
+ name: 'editor',
96
+ permissions: [
97
+ {
98
+ id: 'perm-1',
99
+ name: 'read:posts'
100
+ }
101
+ ]
102
+ },
103
+ {
104
+ id: 'role-2',
105
+ name: 'manager',
106
+ permissions: [
107
+ {
108
+ id: 'perm-2',
109
+ name: 'delete:posts'
110
+ }
111
+ ]
112
+ }
113
+ ]
114
+ };
115
+ expect(hasPermission(user, 'delete:posts')).toBe(true);
116
+ expect(hasPermission(user, 'delete:posts')).toBe(true);
117
+ expect(hasPermission(user, 'read:posts')).toBe(true);
118
+ expect(hasPermission(user, 'create:users')).toBe(false);
119
+ });
120
+ });
121
+
122
+ //# sourceMappingURL=hasPermission.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/hasPermission.spec.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload';\nimport { describe, expect, it } from 'vitest';\nimport { hasPermission } from './hasPermission';\n\ndescribe('hasPermission', () => {\n it('should return false if user is null', () => {\n expect(hasPermission(null, 'read:posts')).toBe(false)\n })\n\n it('should return false if user has no roles array', () => {\n const user = { id: '1' } as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return false if user roles is unpopulated (array of strings)', () => {\n const user = {\n id: '1',\n roles: ['role-id-1'],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return false if role has no permissions', () => {\n const user = {\n id: '1',\n roles: [{ id: 'role-1', name: 'admin' }],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return false if role permissions are unpopulated (array of strings)', () => {\n const user = {\n id: '1',\n roles: [{ id: 'role-1', name: 'admin', permissions: ['perm-id-1'] }],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'read:posts')).toBe(false)\n })\n\n it('should return true if permission is found in populated roles', () => {\n const user = {\n id: '1',\n roles: [\n {\n id: 'role-1',\n name: 'admin',\n permissions: [\n { id: 'perm-1', name: 'read:posts' },\n { id: 'perm-2', name: 'write:posts' },\n ],\n },\n ],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'write:posts')).toBe(true)\n })\n\n it('should return false if permission is not found in populated roles', () => {\n const user = {\n id: '1',\n roles: [\n {\n id: 'role-1',\n name: 'editor',\n permissions: [{ id: 'perm-1', name: 'read:posts' }],\n },\n ],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'delete:posts')).toBe(false)\n })\n\n it('should correctly search through multiple roles', () => {\n const user = {\n id: '1',\n roles: [\n {\n id: 'role-1',\n name: 'editor',\n permissions: [{ id: 'perm-1', name: 'read:posts' }],\n },\n {\n id: 'role-2',\n name: 'manager',\n permissions: [{ id: 'perm-2', name: 'delete:posts' }],\n },\n ],\n } as unknown as PayloadRequest['user']\n expect(hasPermission(user, 'delete:posts')).toBe(true)\n expect(hasPermission(user, 'delete:posts')).toBe(true)\n expect(hasPermission(user, 'read:posts')).toBe(true)\n expect(hasPermission(user, 'create:users')).toBe(false)\n })\n})\n"],"names":["describe","expect","it","hasPermission","toBe","user","id","roles","name","permissions"],"mappings":"AACA,SAASA,QAAQ,EAAEC,MAAM,EAAEC,EAAE,QAAQ,SAAS;AAC9C,SAASC,aAAa,QAAQ,kBAAkB;AAEhDH,SAAS,iBAAiB;IACxBE,GAAG,uCAAuC;QACxCD,OAAOE,cAAc,MAAM,eAAeC,IAAI,CAAC;IACjD;IAEAF,GAAG,kDAAkD;QACnD,MAAMG,OAAO;YAAEC,IAAI;QAAI;QACvBL,OAAOE,cAAcE,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,uEAAuE;QACxE,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBAAC;aAAY;QACtB;QACAN,OAAOE,cAAcE,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,kDAAkD;QACnD,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBAAC;oBAAED,IAAI;oBAAUE,MAAM;gBAAQ;aAAE;QAC1C;QACAP,OAAOE,cAAcE,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,8EAA8E;QAC/E,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBAAC;oBAAED,IAAI;oBAAUE,MAAM;oBAASC,aAAa;wBAAC;qBAAY;gBAAC;aAAE;QACtE;QACAR,OAAOE,cAAcE,MAAM,eAAeD,IAAI,CAAC;IACjD;IAEAF,GAAG,gEAAgE;QACjE,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBACL;oBACED,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBACX;4BAAEH,IAAI;4BAAUE,MAAM;wBAAa;wBACnC;4BAAEF,IAAI;4BAAUE,MAAM;wBAAc;qBACrC;gBACH;aACD;QACH;QACAP,OAAOE,cAAcE,MAAM,gBAAgBD,IAAI,CAAC;IAClD;IAEAF,GAAG,qEAAqE;QACtE,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBACL;oBACED,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBAAC;4BAAEH,IAAI;4BAAUE,MAAM;wBAAa;qBAAE;gBACrD;aACD;QACH;QACAP,OAAOE,cAAcE,MAAM,iBAAiBD,IAAI,CAAC;IACnD;IAEAF,GAAG,kDAAkD;QACnD,MAAMG,OAAO;YACXC,IAAI;YACJC,OAAO;gBACL;oBACED,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBAAC;4BAAEH,IAAI;4BAAUE,MAAM;wBAAa;qBAAE;gBACrD;gBACA;oBACEF,IAAI;oBACJE,MAAM;oBACNC,aAAa;wBAAC;4BAAEH,IAAI;4BAAUE,MAAM;wBAAe;qBAAE;gBACvD;aACD;QACH;QACAP,OAAOE,cAAcE,MAAM,iBAAiBD,IAAI,CAAC;QACjDH,OAAOE,cAAcE,MAAM,iBAAiBD,IAAI,CAAC;QACjDH,OAAOE,cAAcE,MAAM,eAAeD,IAAI,CAAC;QAC/CH,OAAOE,cAAcE,MAAM,iBAAiBD,IAAI,CAAC;IACnD;AACF"}
@@ -0,0 +1,2 @@
1
+ export { checkPermission } from './checkPermission';
2
+ export { hasPermission } from './hasPermission';
@@ -0,0 +1,4 @@
1
+ export { checkPermission } from './checkPermission';
2
+ export { hasPermission } from './hasPermission';
3
+
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utilities/index.ts"],"sourcesContent":["export { checkPermission } from './checkPermission';\nexport { hasPermission } from './hasPermission';\n\n"],"names":["checkPermission","hasPermission"],"mappings":"AAAA,SAASA,eAAe,QAAQ,oBAAoB;AACpD,SAASC,aAAa,QAAQ,kBAAkB"}
package/package.json ADDED
@@ -0,0 +1,142 @@
1
+ {
2
+ "name": "payload-rbac-plugin",
3
+ "version": "0.0.1",
4
+ "description": "RBAC plugin for payloadcms",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/Mhmod-Hsn/Payload-RBAC-Plugin.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/Mhmod-Hsn/Payload-RBAC-Plugin/issues"
11
+ },
12
+ "homepage": "https://github.com/Mhmod-Hsn/Payload-RBAC-Plugin#readme",
13
+ "license": "MIT",
14
+ "author": "Mahmoud Hassan <hsn.mhmod.96@gmail.com> (https://github.com/Mhmod-Hsn)",
15
+ "keywords": [
16
+ "payload",
17
+ "payloadcms",
18
+ "payload-plugin",
19
+ "plugin",
20
+ "rbac",
21
+ "role-based-access-control",
22
+ "access-control",
23
+ "permissions",
24
+ "roles",
25
+ "security",
26
+ "authorization",
27
+ "auth",
28
+ "typescript"
29
+ ],
30
+ "type": "module",
31
+ "exports": {
32
+ ".": {
33
+ "import": "./src/index.ts",
34
+ "types": "./src/index.ts",
35
+ "default": "./src/index.ts"
36
+ },
37
+ "./client": {
38
+ "import": "./src/exports/client.ts",
39
+ "types": "./src/exports/client.ts",
40
+ "default": "./src/exports/client.ts"
41
+ },
42
+ "./rsc": {
43
+ "import": "./src/exports/rsc.ts",
44
+ "types": "./src/exports/rsc.ts",
45
+ "default": "./src/exports/rsc.ts"
46
+ }
47
+ },
48
+ "main": "./src/index.ts",
49
+ "types": "./src/index.ts",
50
+ "files": [
51
+ "dist"
52
+ ],
53
+ "scripts": {
54
+ "build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
55
+ "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
56
+ "build:types": "tsc --outDir dist --rootDir ./src",
57
+ "clean": "rimraf {dist,*.tsbuildinfo}",
58
+ "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
59
+ "dev": "next dev dev --turbo",
60
+ "dev:generate-importmap": "pnpm dev:payload generate:importmap",
61
+ "dev:generate-types": "pnpm dev:payload generate:types",
62
+ "dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
63
+ "generate:importmap": "pnpm dev:generate-importmap",
64
+ "generate:types": "pnpm dev:generate-types",
65
+ "lint": "eslint",
66
+ "lint:fix": "eslint ./src --fix",
67
+ "test": "pnpm test:int && pnpm test:e2e",
68
+ "test:e2e": "playwright test",
69
+ "test:int": "vitest"
70
+ },
71
+ "devDependencies": {
72
+ "@eslint/eslintrc": "^3.2.0",
73
+ "@payloadcms/db-mongodb": "3.84.1",
74
+ "@payloadcms/db-postgres": "3.84.1",
75
+ "@payloadcms/db-sqlite": "3.84.1",
76
+ "@payloadcms/eslint-config": "3.28.0",
77
+ "@payloadcms/next": "3.84.1",
78
+ "@payloadcms/richtext-lexical": "3.84.1",
79
+ "@payloadcms/ui": "3.84.1",
80
+ "@playwright/test": "1.58.2",
81
+ "@swc-node/register": "1.10.9",
82
+ "@swc/cli": "0.6.0",
83
+ "@types/node": "22.19.9",
84
+ "@types/react": "19.2.14",
85
+ "@types/react-dom": "19.2.3",
86
+ "copyfiles": "2.4.1",
87
+ "cross-env": "^7.0.3",
88
+ "eslint": "^9.23.0",
89
+ "eslint-config-next": "16.2.6",
90
+ "graphql": "^16.8.1",
91
+ "mongodb-memory-server": "10.1.4",
92
+ "next": "16.2.6",
93
+ "open": "^10.1.0",
94
+ "payload": "3.84.1",
95
+ "prettier": "^3.4.2",
96
+ "qs-esm": "8.0.1",
97
+ "react": "19.2.6",
98
+ "react-dom": "19.2.6",
99
+ "rimraf": "3.0.2",
100
+ "sharp": "0.34.2",
101
+ "sort-package-json": "^2.10.0",
102
+ "typescript": "5.7.3",
103
+ "vite-tsconfig-paths": "6.0.5",
104
+ "vitest": "^4.1.8"
105
+ },
106
+ "peerDependencies": {
107
+ "payload": "^3.84.1"
108
+ },
109
+ "engines": {
110
+ "node": "^18.20.2 || >=20.9.0",
111
+ "pnpm": "^9 || ^10"
112
+ },
113
+ "publishConfig": {
114
+ "exports": {
115
+ ".": {
116
+ "import": "./dist/index.js",
117
+ "types": "./dist/index.d.ts",
118
+ "default": "./dist/index.js"
119
+ },
120
+ "./client": {
121
+ "import": "./dist/exports/client.js",
122
+ "types": "./dist/exports/client.d.ts",
123
+ "default": "./dist/exports/client.js"
124
+ },
125
+ "./rsc": {
126
+ "import": "./dist/exports/rsc.js",
127
+ "types": "./dist/exports/rsc.d.ts",
128
+ "default": "./dist/exports/rsc.js"
129
+ }
130
+ },
131
+ "main": "./dist/index.js",
132
+ "types": "./dist/index.d.ts"
133
+ },
134
+ "pnpm": {
135
+ "onlyBuiltDependencies": [
136
+ "sharp",
137
+ "esbuild",
138
+ "unrs-resolver"
139
+ ]
140
+ },
141
+ "registry": "https://registry.npmjs.org/"
142
+ }