@plakboek/permissions 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Florian Vanthuyne
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,171 @@
1
+ # @plakboek/permissions
2
+
3
+ The fixed permission catalogue and code-defined role system for Plakboek
4
+ CMS installations.
5
+
6
+ ## What this package is
7
+
8
+ `@plakboek/permissions` owns a single, frozen catalogue of every permission
9
+ string Plakboek understands (`resource:action`, e.g. `pages:publish`). The
10
+ catalogue is **exported by this package and cannot be extended, renamed, or
11
+ altered per installation** (USER-05) -- it ships with the CMS core, the same
12
+ for every install. What an installation _can_ configure in code is which
13
+ role holds which of these permissions (`defineRoles`), never the catalogue
14
+ itself.
15
+
16
+ The package has zero runtime dependencies and does no I/O: everything here
17
+ is pure, synchronous, in-memory validation and lookup.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ pnpm add @plakboek/permissions
23
+ ```
24
+
25
+ ## Permissions
26
+
27
+ The table below is generated from the catalogue (`PERMISSIONS`) and the
28
+ shipped default roles (`defaultRoles`) -- see `docs:permissions` below for
29
+ how it stays in sync.
30
+
31
+ <!-- permissions-table:start -->
32
+
33
+ | Permission | Group | Description | superadmin | admin | editor |
34
+ | -------------------------- | ------------- | -------------------------------------------------------------------------- | ---------- | ----- | ------ |
35
+ | `pages:read` | pages | See pages and their published content in the admin | ✓ | ✓ | ✓ |
36
+ | `pages:read-drafts` | pages | See unpublished drafts, previews and revision history of pages | ✓ | ✓ | ✓ |
37
+ | `pages:create` | pages | Add new pages to the site | ✓ | ✓ | ✓ |
38
+ | `pages:edit` | pages | Change page content, blocks, layout, SEO fields and translations as drafts | ✓ | ✓ | ✓ |
39
+ | `pages:publish` | pages | Publish or schedule page drafts so visitors receive them | ✓ | ✓ | ✓ |
40
+ | `pages:delete` | pages | Move pages to the trash and restore them | ✓ | ✓ | ✓ |
41
+ | `pages:delete-permanent` | pages | Permanently remove trashed pages | ✓ | | |
42
+ | `entries:read` | entries | See content entries and their published content in the admin | ✓ | ✓ | ✓ |
43
+ | `entries:read-drafts` | entries | See unpublished drafts, previews and revision history of entries | ✓ | ✓ | ✓ |
44
+ | `entries:create` | entries | Add new content entries | ✓ | ✓ | ✓ |
45
+ | `entries:edit` | entries | Change entry fields and translations as drafts | ✓ | ✓ | ✓ |
46
+ | `entries:publish` | entries | Publish or schedule entry drafts so visitors receive them | ✓ | ✓ | ✓ |
47
+ | `entries:delete` | entries | Move entries to the trash and restore them | ✓ | ✓ | ✓ |
48
+ | `entries:delete-permanent` | entries | Permanently remove trashed entries | ✓ | | |
49
+ | `templates:edit` | templates | Change the shared block tree used to render a content type's detail pages | ✓ | ✓ | ✓ |
50
+ | `templates:publish` | templates | Publish changes to shared detail templates | ✓ | ✓ | ✓ |
51
+ | `content-types:read` | content-types | See content types and the fields attached to them | ✓ | ✓ | ✓ |
52
+ | `content-types:create` | content-types | Add new content types | ✓ | ✓ | |
53
+ | `content-types:edit` | content-types | Change content type settings and add, change or remove fields | ✓ | ✓ | |
54
+ | `content-types:delete` | content-types | Remove content types | ✓ | ✓ | |
55
+ | `media:read` | media | Browse and search the media library | ✓ | ✓ | ✓ |
56
+ | `media:upload` | media | Upload images and files | ✓ | ✓ | ✓ |
57
+ | `media:edit` | media | Change alt text, copyright labels and replace assets in place | ✓ | ✓ | ✓ |
58
+ | `media:delete` | media | Delete assets, including ones still in use after a warning | ✓ | ✓ | |
59
+ | `menus:read` | menus | See menus declared by the site and their links | ✓ | ✓ | ✓ |
60
+ | `menus:edit` | menus | Add, remove and re-arrange menu links | ✓ | ✓ | ✓ |
61
+ | `redirects:read` | seo | See redirects and how long each has been active | ✓ | ✓ | ✓ |
62
+ | `redirects:manage` | seo | Create, change and remove redirects | ✓ | ✓ | |
63
+ | `users:read` | users | See users and their roles | ✓ | ✓ | |
64
+ | `users:create` | users | Add users and send or resend set-password emails | ✓ | ✓ | |
65
+ | `users:edit` | users | Change other users' profile details | ✓ | ✓ | |
66
+ | `users:deactivate` | users | Deactivate and reactivate users | ✓ | ✓ | |
67
+ | `users:assign-roles` | users | Assign a code-defined role to a user | ✓ | ✓ | |
68
+ | `users:reset-password` | users | Set or reset another user's password | ✓ | ✓ | |
69
+ | `users:impersonate` | users | Act as another user and return to your own session | ✓ | | |
70
+ | `audit-log:read` | audit | See the audit log of data-changing actions | ✓ | ✓ | |
71
+ | `settings:read` | settings | See installation settings | ✓ | ✓ | |
72
+ | `settings:manage` | settings | Change core configuration, styles and rules | ✓ | ✓ | |
73
+ | `modules:manage` | settings | Turn installation modules on or off | ✓ | | |
74
+ | `blocks:manage` | settings | Turn individual built-in sections and blocks on or off | ✓ | | |
75
+ | `bootstrap:run` | settings | Run installation bootstrap steps | ✓ | | |
76
+ | `api-tokens:create` | api | Create and revoke your own API tokens for MCP clients | ✓ | ✓ | ✓ |
77
+ | `api-tokens:manage` | api | See and revoke any user's API tokens | ✓ | ✓ | |
78
+ | `backups:manage` | backups | Configure backup targets and run backups | ✓ | | |
79
+ | `backups:restore` | backups | Restore the installation from a backup | ✓ | | |
80
+
81
+ <!-- permissions-table:end -->
82
+
83
+ ## Defining roles in code
84
+
85
+ Roles map a role key to a list of permissions, entirely in your
86
+ installation's own code -- there is no database table and no admin UI for
87
+ changing what a role _can_ do (only which role a _user_ holds is data,
88
+ handled elsewhere). The shipped defaults work unmodified; add or replace
89
+ roles by spreading `defaultRoles`:
90
+
91
+ ```ts
92
+ import {
93
+ defineRoles,
94
+ defaultRoles,
95
+ ALL_PERMISSIONS,
96
+ } from '@plakboek/permissions';
97
+
98
+ const roles = defineRoles({
99
+ ...defaultRoles,
100
+ // A host-specific role: exactly these two permissions, nothing inherited.
101
+ client: ['pages:read', 'pages:edit'],
102
+ });
103
+ ```
104
+
105
+ `superadmin` is reserved and required: it must be present and must equal
106
+ `ALL_PERMISSIONS` exactly (D-11, D-12) --
107
+
108
+ ```ts
109
+ defineRoles({
110
+ superadmin: ALL_PERMISSIONS,
111
+ // ...
112
+ });
113
+ ```
114
+
115
+ -- `defineRoles` throws a single `RoleConfigError` (with a `.issues` array
116
+ listing every problem found) if `superadmin` is missing, narrowed, or if any
117
+ role references an unknown permission string. There is no inheritance,
118
+ `extends`, or wildcard (D-14): a role's permissions are exactly what its
119
+ list contains, so any role can be audited by reading one array.
120
+
121
+ ## Orphaned role keys
122
+
123
+ A user's stored role key (Phase 2) can outlive the role it once named --
124
+ the installation's role config changed, or the value was mistyped upstream.
125
+ `createPermissionResolver` never throws for this case: an unknown role key
126
+ resolves to an **empty permission set**, and the event is reported once,
127
+ rate-limited per key, through an injectable hook (D-15):
128
+
129
+ ```ts
130
+ import { createPermissionResolver } from '@plakboek/permissions';
131
+
132
+ const resolver = createPermissionResolver(roles, {
133
+ onOrphanedRole(event) {
134
+ // event: { roleKey, userId?, occurredAt, suppressedCount }
135
+ },
136
+ rateLimitMs: 60_000, // default: one warning per role key per 60s
137
+ });
138
+
139
+ resolver.resolve('some-role-key', { userId: user.id });
140
+ ```
141
+
142
+ `OrphanedRoleEvent` carries only `roleKey`, the opaque `userId` (when
143
+ known), `occurredAt`, and `suppressedCount` -- never any other user data.
144
+ Without a hook, the resolver logs via `console.warn`. A hook that throws is
145
+ caught and never propagates into the permission check itself: the empty
146
+ set is already decided before the hook runs.
147
+
148
+ Phase 2's boot-time scan of stored role keys and the Phase 10/11 admin
149
+ banner both consume this exact hook rather than re-implementing orphaned
150
+ role detection.
151
+
152
+ ## Stability policy
153
+
154
+ Permission strings are a published contract every host's role config
155
+ depends on. Adding a permission is a minor version bump. Renaming or
156
+ removing one is a major version bump -- **while the package is pre-1.0, a
157
+ rename/removal is a minor bump instead** -- and the old name is kept as a
158
+ deprecated alias for one full major version after the rename/removal ships.
159
+ A host config that still references the old name keeps booting, with a
160
+ warning, for that entire window; it is never rejected outright.
161
+
162
+ ## Security notes
163
+
164
+ `users:assign-roles` lets its holder assign **any** code-defined role to a
165
+ user -- including roles more privileged than their own. This package does
166
+ not, and cannot, prevent that: it has no concept of "the currently acting
167
+ user" or which permissions they hold at the point of assignment. Wherever
168
+ role assignment is implemented (Phase 11), the handler **must** refuse to
169
+ assign a role holding any permission the assigning user does not themselves
170
+ hold -- otherwise a user with `users:assign-roles` could grant themselves
171
+ `superadmin` (USER-02).