@plutocms/pluto 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## [0.5.0](https://github.com/plutocms/pluto/compare/v0.4.0...v0.5.0) (2026-09-12)
5
+
6
+
7
+ ### Features
8
+
9
+ * **permissions:** add capability registry and usePlutoPermissions composable ([#129](https://github.com/plutocms/pluto/issues/129)) ([c98419f](https://github.com/plutocms/pluto/commit/c98419fc967f8e20678389169e937db759c308bc))
10
+
4
11
  ## [0.4.0](https://github.com/plutocms/pluto/compare/v0.3.2...v0.4.0) (2026-09-12)
5
12
 
6
13
 
package/FEATURES.md CHANGED
@@ -9,3 +9,6 @@ policies shared by every project here.
9
9
  - Extension registry: a typed, ordered, reactive registry that lets a layer plug its admin-UI
10
10
  pieces (navbar, sidebar nav, pages, dashboard widgets, settings panels and driver) into core
11
11
  with one function call. @.claude/skills/extension-registry/SKILL.md
12
+ - Permissions: a typed capability registry and a `usePlutoPermissions()` composable that let a
13
+ layer declare capabilities and check what the current user holds, backed by a registered
14
+ permissions driver. @.claude/skills/permissions/SKILL.md
@@ -21,4 +21,9 @@ export function definePlutoExtension(definition: PlutoExtension): void {
21
21
  definition.settingsDriver ? [definition.settingsDriver] : []
22
22
  )
23
23
  registry.dashboardWidgets.set(definition.id, definition.dashboardWidgets ?? [])
24
+ registry.capabilities.set(definition.id, definition.capabilities ?? [])
25
+ registry.permissionsDrivers.set(
26
+ definition.id,
27
+ definition.permissionsDriver ? [definition.permissionsDriver] : []
28
+ )
24
29
  }
@@ -0,0 +1,74 @@
1
+ /** Capabilities declared by every layer, in registry order. */
2
+ export function usePlutoCapabilities() {
3
+ const registry = usePlutoRegistry()
4
+ return { items: registry.capabilities.list }
5
+ }
6
+
7
+ /**
8
+ * The current user's capabilities, backed by whichever permissions driver
9
+ * is registered. `useState` is correct for the loaded list itself (plain
10
+ * strings, and /admin/** is client-only per nuxt.config.ts) — this
11
+ * mirrors usePlutoSettings()'s values/dirty state exactly. The driver
12
+ * registration itself goes through the registry, same as
13
+ * PlutoSettingsDriver.
14
+ */
15
+ export function usePlutoPermissions() {
16
+ const registry = usePlutoRegistry()
17
+ const driver = computed(() => registry.permissionsDrivers.list.value.at(-1) ?? null)
18
+
19
+ const granted = useState<string[]>('pluto-permissions-granted', () => [])
20
+ const loaded = useState<boolean>('pluto-permissions-loaded', () => false)
21
+ const status = ref<'idle' | 'loading'>('idle')
22
+
23
+ async function load() {
24
+ if (!driver.value) {
25
+ return
26
+ }
27
+
28
+ status.value = 'loading'
29
+ try {
30
+ granted.value = await driver.value.load()
31
+ loaded.value = true
32
+ } finally {
33
+ status.value = 'idle'
34
+ }
35
+ }
36
+
37
+ function clear() {
38
+ granted.value = []
39
+ loaded.value = false
40
+ }
41
+
42
+ /**
43
+ * True when the user holds this capability.
44
+ *
45
+ * Fails OPEN when no permissions driver is registered — a site with no
46
+ * permissions backend also has no server-side guard, so hiding UI in
47
+ * that case would only be theatre, and would break a core-only
48
+ * install with nothing registered yet. Read `hasDriver` to tell the two
49
+ * cases apart if a caller needs to.
50
+ *
51
+ * This is never a security boundary by itself. The server-side guard
52
+ * (a backend layer's own requireCapability-equivalent, backed by RLS)
53
+ * is what actually enforces anything. This only decides what the UI
54
+ * shows.
55
+ */
56
+ function can(capability: string): boolean {
57
+ if (!driver.value) {
58
+ return true
59
+ }
60
+
61
+ return granted.value.includes('*') || granted.value.includes(capability)
62
+ }
63
+
64
+ return {
65
+ granted,
66
+ loaded,
67
+ status,
68
+ hasDriver: computed(() => Boolean(driver.value)),
69
+ isAdmin: computed(() => granted.value.includes('*')),
70
+ load,
71
+ clear,
72
+ can,
73
+ }
74
+ }
@@ -21,6 +21,8 @@ export function usePlutoRegistry(): PlutoRegistry {
21
21
  settingsPanels: createOwnedRegistry<PlutoSettingsPanel>(),
22
22
  settingsDrivers: createOwnedRegistry<PlutoSettingsDriver>(),
23
23
  dashboardWidgets: createOwnedRegistry<PlutoDashboardWidget>(),
24
+ capabilities: createOwnedRegistry<PlutoCapability>(),
25
+ permissionsDrivers: createOwnedRegistry<PlutoPermissionsDriver>(),
24
26
  }
25
27
  }
26
28
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@plutocms/pluto",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.5.0",
5
5
  "description": "The layerable CMS",
6
6
  "author": "Victor Ribeiro",
7
7
  "license": "MIT",
@@ -59,6 +59,32 @@ export interface PlutoSettingsDriver extends PlutoEntry {
59
59
  save: (patch: Record<string, string>) => Promise<void>
60
60
  }
61
61
 
62
+ /**
63
+ * A capability a layer defines. `key` is the real capability string, for
64
+ * example 'posts:publish'. It is kept separate from `id` on purpose: the
65
+ * registry rewrites `id` to '<extensionId>:<id>' (see registry-factory.ts),
66
+ * which would corrupt a capability string if `id` were reused as the
67
+ * capability itself. `id` identifies this registry entry; `key` identifies
68
+ * the permission.
69
+ */
70
+ export interface PlutoCapability extends PlutoEntry {
71
+ /** Format: '<namespace>:<action>', lowercase, for example 'posts:publish'. */
72
+ key: string
73
+ label: string
74
+ description?: string
75
+ }
76
+
77
+ /**
78
+ * Loads the current user's capability list. One backend layer registers
79
+ * this, the same way one layer registers a PlutoSettingsDriver. `load`
80
+ * returns a flat list of capability key strings the user holds. The
81
+ * single entry '*' means "every capability" (used by the built-in admin
82
+ * role).
83
+ */
84
+ export interface PlutoPermissionsDriver extends PlutoEntry {
85
+ load: () => Promise<string[]>
86
+ }
87
+
62
88
  export interface PlutoExtension {
63
89
  /** Layer name. Use the package's $meta.name, e.g. 'supabase-blog'. */
64
90
  id: string
@@ -69,6 +95,8 @@ export interface PlutoExtension {
69
95
  dashboardWidgets?: PlutoDashboardWidget[]
70
96
  settingsPanels?: PlutoSettingsPanel[]
71
97
  settingsDriver?: PlutoSettingsDriver
98
+ capabilities?: PlutoCapability[]
99
+ permissionsDriver?: PlutoPermissionsDriver
72
100
  }
73
101
 
74
102
  /**
@@ -91,4 +119,6 @@ export interface PlutoRegistry {
91
119
  settingsPanels: PlutoRegistryBucket<PlutoSettingsPanel>
92
120
  settingsDrivers: PlutoRegistryBucket<PlutoSettingsDriver>
93
121
  dashboardWidgets: PlutoRegistryBucket<PlutoDashboardWidget>
122
+ capabilities: PlutoRegistryBucket<PlutoCapability>
123
+ permissionsDrivers: PlutoRegistryBucket<PlutoPermissionsDriver>
94
124
  }