@pyreon/permissions 0.11.5 → 0.11.7

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.
@@ -1,64 +1,64 @@
1
- import { signal } from "@pyreon/reactivity"
2
- import { describe, expect, it } from "vitest"
3
- import { createPermissions } from "../index"
1
+ import { signal } from '@pyreon/reactivity'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { createPermissions } from '../index'
4
4
 
5
- describe("createPermissions — predicate permissions", () => {
6
- describe("basic predicate evaluation", () => {
7
- it("evaluates predicate with context argument", () => {
5
+ describe('createPermissions — predicate permissions', () => {
6
+ describe('basic predicate evaluation', () => {
7
+ it('evaluates predicate with context argument', () => {
8
8
  const can = createPermissions({
9
- "posts.update": (post: any) => post?.authorId === "user-1",
9
+ 'posts.update': (post: any) => post?.authorId === 'user-1',
10
10
  })
11
- expect(can("posts.update", { authorId: "user-1" })).toBe(true)
12
- expect(can("posts.update", { authorId: "user-2" })).toBe(false)
11
+ expect(can('posts.update', { authorId: 'user-1' })).toBe(true)
12
+ expect(can('posts.update', { authorId: 'user-2' })).toBe(false)
13
13
  })
14
14
 
15
- it("predicate called without context receives undefined", () => {
15
+ it('predicate called without context receives undefined', () => {
16
16
  const can = createPermissions({
17
- "posts.update": (post?: any) => post?.authorId === "user-1",
17
+ 'posts.update': (post?: any) => post?.authorId === 'user-1',
18
18
  })
19
19
  // No context — post is undefined
20
- expect(can("posts.update")).toBe(false)
20
+ expect(can('posts.update')).toBe(false)
21
21
  })
22
22
 
23
- it("predicate that ignores context (always true)", () => {
23
+ it('predicate that ignores context (always true)', () => {
24
24
  const can = createPermissions({
25
- "posts.create": () => true,
25
+ 'posts.create': () => true,
26
26
  })
27
- expect(can("posts.create")).toBe(true)
28
- expect(can("posts.create", { anything: true })).toBe(true)
27
+ expect(can('posts.create')).toBe(true)
28
+ expect(can('posts.create', { anything: true })).toBe(true)
29
29
  })
30
30
 
31
- it("predicate that ignores context (always false)", () => {
31
+ it('predicate that ignores context (always false)', () => {
32
32
  const can = createPermissions({
33
- "posts.create": () => false,
33
+ 'posts.create': () => false,
34
34
  })
35
- expect(can("posts.create")).toBe(false)
35
+ expect(can('posts.create')).toBe(false)
36
36
  })
37
37
  })
38
38
 
39
- describe("complex predicate logic", () => {
40
- it("ownership check", () => {
41
- const currentUserId = "user-42"
39
+ describe('complex predicate logic', () => {
40
+ it('ownership check', () => {
41
+ const currentUserId = 'user-42'
42
42
  const can = createPermissions({
43
- "posts.update": (post: any) => post?.authorId === currentUserId,
44
- "posts.delete": (post: any) => post?.authorId === currentUserId && post?.status === "draft",
43
+ 'posts.update': (post: any) => post?.authorId === currentUserId,
44
+ 'posts.delete': (post: any) => post?.authorId === currentUserId && post?.status === 'draft',
45
45
  })
46
46
 
47
- const myDraft = { authorId: "user-42", status: "draft" }
48
- const myPublished = { authorId: "user-42", status: "published" }
49
- const otherPost = { authorId: "user-99", status: "draft" }
47
+ const myDraft = { authorId: 'user-42', status: 'draft' }
48
+ const myPublished = { authorId: 'user-42', status: 'published' }
49
+ const otherPost = { authorId: 'user-99', status: 'draft' }
50
50
 
51
- expect(can("posts.update", myDraft)).toBe(true)
52
- expect(can("posts.update", myPublished)).toBe(true)
53
- expect(can("posts.update", otherPost)).toBe(false)
54
- expect(can("posts.delete", myDraft)).toBe(true)
55
- expect(can("posts.delete", myPublished)).toBe(false)
56
- expect(can("posts.delete", otherPost)).toBe(false)
51
+ expect(can('posts.update', myDraft)).toBe(true)
52
+ expect(can('posts.update', myPublished)).toBe(true)
53
+ expect(can('posts.update', otherPost)).toBe(false)
54
+ expect(can('posts.delete', myDraft)).toBe(true)
55
+ expect(can('posts.delete', myPublished)).toBe(false)
56
+ expect(can('posts.delete', otherPost)).toBe(false)
57
57
  })
58
58
 
59
- it("predicate with multiple conditions", () => {
59
+ it('predicate with multiple conditions', () => {
60
60
  const can = createPermissions({
61
- "orders.refund": (order: any) => {
61
+ 'orders.refund': (order: any) => {
62
62
  if (!order) return false
63
63
  const isRecent = Date.now() - order.createdAt < 86400000
64
64
  const isSmall = order.amount < 100
@@ -70,80 +70,80 @@ describe("createPermissions — predicate permissions", () => {
70
70
  const recentLarge = { createdAt: Date.now() - 1000, amount: 200 }
71
71
  const oldSmall = { createdAt: Date.now() - 100_000_000, amount: 50 }
72
72
 
73
- expect(can("orders.refund", recentSmall)).toBe(true)
74
- expect(can("orders.refund", recentLarge)).toBe(false)
75
- expect(can("orders.refund", oldSmall)).toBe(false)
73
+ expect(can('orders.refund', recentSmall)).toBe(true)
74
+ expect(can('orders.refund', recentLarge)).toBe(false)
75
+ expect(can('orders.refund', oldSmall)).toBe(false)
76
76
  })
77
77
 
78
- it("predicate referencing reactive signal", () => {
79
- const role = signal("admin")
78
+ it('predicate referencing reactive signal', () => {
79
+ const role = signal('admin')
80
80
  const can = createPermissions({
81
- "users.manage": () => role.peek() === "admin",
81
+ 'users.manage': () => role.peek() === 'admin',
82
82
  })
83
83
 
84
- expect(can("users.manage")).toBe(true)
84
+ expect(can('users.manage')).toBe(true)
85
85
 
86
- role.set("viewer")
86
+ role.set('viewer')
87
87
  // Need to trigger version update for reactive tracking,
88
88
  // but predicate re-evaluation on read still reflects signal state
89
- can.patch({ "users.manage": () => role.peek() === "admin" })
90
- expect(can("users.manage")).toBe(false)
89
+ can.patch({ 'users.manage': () => role.peek() === 'admin' })
90
+ expect(can('users.manage')).toBe(false)
91
91
  })
92
92
  })
93
93
 
94
- describe("predicate error handling", () => {
95
- it("returns false when predicate throws", () => {
94
+ describe('predicate error handling', () => {
95
+ it('returns false when predicate throws', () => {
96
96
  const can = createPermissions({
97
- "posts.update": () => {
98
- throw new Error("boom")
97
+ 'posts.update': () => {
98
+ throw new Error('boom')
99
99
  },
100
100
  })
101
- expect(can("posts.update")).toBe(false)
101
+ expect(can('posts.update')).toBe(false)
102
102
  })
103
103
 
104
- it("returns false when predicate throws with context", () => {
104
+ it('returns false when predicate throws with context', () => {
105
105
  const can = createPermissions({
106
- "posts.update": (_post: any) => {
107
- throw new TypeError("property access failed")
106
+ 'posts.update': (_post: any) => {
107
+ throw new TypeError('property access failed')
108
108
  },
109
109
  })
110
- expect(can("posts.update", { id: 1 })).toBe(false)
110
+ expect(can('posts.update', { id: 1 })).toBe(false)
111
111
  })
112
112
 
113
- it("can.not returns true when predicate throws", () => {
113
+ it('can.not returns true when predicate throws', () => {
114
114
  const can = createPermissions({
115
- "posts.update": () => {
116
- throw new Error("error")
115
+ 'posts.update': () => {
116
+ throw new Error('error')
117
117
  },
118
118
  })
119
- expect(can.not("posts.update")).toBe(true)
119
+ expect(can.not('posts.update')).toBe(true)
120
120
  })
121
121
 
122
- it("other permissions unaffected when one predicate throws", () => {
122
+ it('other permissions unaffected when one predicate throws', () => {
123
123
  const can = createPermissions({
124
- "posts.read": true,
125
- "posts.update": () => {
126
- throw new Error("boom")
124
+ 'posts.read': true,
125
+ 'posts.update': () => {
126
+ throw new Error('boom')
127
127
  },
128
- "posts.create": () => true,
128
+ 'posts.create': () => true,
129
129
  })
130
- expect(can("posts.read")).toBe(true)
131
- expect(can("posts.update")).toBe(false)
132
- expect(can("posts.create")).toBe(true)
130
+ expect(can('posts.read')).toBe(true)
131
+ expect(can('posts.update')).toBe(false)
132
+ expect(can('posts.create')).toBe(true)
133
133
  })
134
134
  })
135
135
 
136
- describe("predicates in granted()", () => {
137
- it("predicates count as capabilities in granted()", () => {
136
+ describe('predicates in granted()', () => {
137
+ it('predicates count as capabilities in granted()', () => {
138
138
  const can = createPermissions({
139
- "posts.read": true,
140
- "posts.update": (post: any) => post?.authorId === "me",
141
- "posts.delete": false,
139
+ 'posts.read': true,
140
+ 'posts.update': (post: any) => post?.authorId === 'me',
141
+ 'posts.delete': false,
142
142
  })
143
143
  const granted = can.granted()
144
- expect(granted).toContain("posts.read")
145
- expect(granted).toContain("posts.update") // predicate = capability exists
146
- expect(granted).not.toContain("posts.delete")
144
+ expect(granted).toContain('posts.read')
145
+ expect(granted).toContain('posts.update') // predicate = capability exists
146
+ expect(granted).not.toContain('posts.delete')
147
147
  })
148
148
  })
149
149
  })
@@ -1,141 +1,141 @@
1
- import { effect } from "@pyreon/reactivity"
2
- import { describe, expect, it } from "vitest"
3
- import { createPermissions } from "../index"
4
-
5
- describe("createPermissions — wildcard matching", () => {
6
- describe("prefix wildcard (posts.*)", () => {
7
- it("matches any sub-key under the prefix", () => {
8
- const can = createPermissions({ "posts.*": true })
9
- expect(can("posts.read")).toBe(true)
10
- expect(can("posts.create")).toBe(true)
11
- expect(can("posts.update")).toBe(true)
12
- expect(can("posts.delete")).toBe(true)
1
+ import { effect } from '@pyreon/reactivity'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { createPermissions } from '../index'
4
+
5
+ describe('createPermissions — wildcard matching', () => {
6
+ describe('prefix wildcard (posts.*)', () => {
7
+ it('matches any sub-key under the prefix', () => {
8
+ const can = createPermissions({ 'posts.*': true })
9
+ expect(can('posts.read')).toBe(true)
10
+ expect(can('posts.create')).toBe(true)
11
+ expect(can('posts.update')).toBe(true)
12
+ expect(can('posts.delete')).toBe(true)
13
13
  })
14
14
 
15
- it("does not match keys without a dot", () => {
16
- const can = createPermissions({ "posts.*": true })
17
- expect(can("posts")).toBe(false) // no dot, no wildcard match
15
+ it('does not match keys without a dot', () => {
16
+ const can = createPermissions({ 'posts.*': true })
17
+ expect(can('posts')).toBe(false) // no dot, no wildcard match
18
18
  })
19
19
 
20
- it("does not match different prefix", () => {
21
- const can = createPermissions({ "posts.*": true })
22
- expect(can("users.read")).toBe(false)
23
- expect(can("comments.read")).toBe(false)
20
+ it('does not match different prefix', () => {
21
+ const can = createPermissions({ 'posts.*': true })
22
+ expect(can('users.read')).toBe(false)
23
+ expect(can('comments.read')).toBe(false)
24
24
  })
25
25
 
26
- it("does not match partial prefix overlap", () => {
27
- const can = createPermissions({ "post.*": true })
28
- expect(can("posts.read")).toBe(false) // 'posts' !== 'post'
26
+ it('does not match partial prefix overlap', () => {
27
+ const can = createPermissions({ 'post.*': true })
28
+ expect(can('posts.read')).toBe(false) // 'posts' !== 'post'
29
29
  })
30
30
 
31
- it("exact match takes precedence over wildcard", () => {
31
+ it('exact match takes precedence over wildcard', () => {
32
32
  const can = createPermissions({
33
- "posts.*": true,
34
- "posts.delete": false,
33
+ 'posts.*': true,
34
+ 'posts.delete': false,
35
35
  })
36
- expect(can("posts.read")).toBe(true)
37
- expect(can("posts.create")).toBe(true)
38
- expect(can("posts.delete")).toBe(false) // exact overrides wildcard
36
+ expect(can('posts.read')).toBe(true)
37
+ expect(can('posts.create')).toBe(true)
38
+ expect(can('posts.delete')).toBe(false) // exact overrides wildcard
39
39
  })
40
40
 
41
- it("wildcard with predicate function", () => {
41
+ it('wildcard with predicate function', () => {
42
42
  const can = createPermissions({
43
- "posts.*": (post: any) => post?.status !== "archived",
43
+ 'posts.*': (post: any) => post?.status !== 'archived',
44
44
  })
45
- expect(can("posts.read", { status: "published" })).toBe(true)
46
- expect(can("posts.update", { status: "archived" })).toBe(false)
47
- expect(can("posts.delete", { status: "draft" })).toBe(true)
45
+ expect(can('posts.read', { status: 'published' })).toBe(true)
46
+ expect(can('posts.update', { status: 'archived' })).toBe(false)
47
+ expect(can('posts.delete', { status: 'draft' })).toBe(true)
48
48
  })
49
49
 
50
- it("multiple prefix wildcards for different namespaces", () => {
50
+ it('multiple prefix wildcards for different namespaces', () => {
51
51
  const can = createPermissions({
52
- "posts.*": true,
53
- "users.*": false,
52
+ 'posts.*': true,
53
+ 'users.*': false,
54
54
  })
55
- expect(can("posts.read")).toBe(true)
56
- expect(can("users.read")).toBe(false)
55
+ expect(can('posts.read')).toBe(true)
56
+ expect(can('users.read')).toBe(false)
57
57
  })
58
58
  })
59
59
 
60
- describe("global wildcard (*)", () => {
61
- it("matches any key", () => {
62
- const can = createPermissions({ "*": true })
63
- expect(can("posts.read")).toBe(true)
64
- expect(can("users.manage")).toBe(true)
65
- expect(can("anything.at.all")).toBe(true)
66
- expect(can("simple-key")).toBe(true)
60
+ describe('global wildcard (*)', () => {
61
+ it('matches any key', () => {
62
+ const can = createPermissions({ '*': true })
63
+ expect(can('posts.read')).toBe(true)
64
+ expect(can('users.manage')).toBe(true)
65
+ expect(can('anything.at.all')).toBe(true)
66
+ expect(can('simple-key')).toBe(true)
67
67
  })
68
68
 
69
- it("exact match takes precedence over global wildcard", () => {
69
+ it('exact match takes precedence over global wildcard', () => {
70
70
  const can = createPermissions({
71
- "*": true,
72
- "billing.export": false,
71
+ '*': true,
72
+ 'billing.export': false,
73
73
  })
74
- expect(can("posts.read")).toBe(true)
75
- expect(can("billing.export")).toBe(false)
74
+ expect(can('posts.read')).toBe(true)
75
+ expect(can('billing.export')).toBe(false)
76
76
  })
77
77
 
78
- it("prefix wildcard takes precedence over global wildcard", () => {
78
+ it('prefix wildcard takes precedence over global wildcard', () => {
79
79
  const can = createPermissions({
80
- "*": false,
81
- "posts.*": true,
80
+ '*': false,
81
+ 'posts.*': true,
82
82
  })
83
- expect(can("posts.read")).toBe(true)
84
- expect(can("posts.create")).toBe(true)
85
- expect(can("users.manage")).toBe(false) // falls to global *
83
+ expect(can('posts.read')).toBe(true)
84
+ expect(can('posts.create')).toBe(true)
85
+ expect(can('users.manage')).toBe(false) // falls to global *
86
86
  })
87
87
 
88
- it("resolution order: exact > prefix wildcard > global wildcard", () => {
88
+ it('resolution order: exact > prefix wildcard > global wildcard', () => {
89
89
  const can = createPermissions({
90
- "*": false,
91
- "posts.*": true,
92
- "posts.delete": false,
90
+ '*': false,
91
+ 'posts.*': true,
92
+ 'posts.delete': false,
93
93
  })
94
- expect(can("posts.read")).toBe(true) // matched by posts.*
95
- expect(can("posts.delete")).toBe(false) // exact match
96
- expect(can("users.manage")).toBe(false) // matched by *
94
+ expect(can('posts.read')).toBe(true) // matched by posts.*
95
+ expect(can('posts.delete')).toBe(false) // exact match
96
+ expect(can('users.manage')).toBe(false) // matched by *
97
97
  })
98
98
 
99
- it("global wildcard with predicate", () => {
99
+ it('global wildcard with predicate', () => {
100
100
  const can = createPermissions({
101
- "*": (ctx: any) => ctx?.role === "admin",
101
+ '*': (ctx: any) => ctx?.role === 'admin',
102
102
  })
103
- expect(can("posts.read", { role: "admin" })).toBe(true)
104
- expect(can("posts.read", { role: "viewer" })).toBe(false)
103
+ expect(can('posts.read', { role: 'admin' })).toBe(true)
104
+ expect(can('posts.read', { role: 'viewer' })).toBe(false)
105
105
  })
106
106
  })
107
107
 
108
- describe("wildcard reactivity", () => {
109
- it("wildcards are reactive after set()", () => {
110
- const can = createPermissions({ "posts.*": false })
108
+ describe('wildcard reactivity', () => {
109
+ it('wildcards are reactive after set()', () => {
110
+ const can = createPermissions({ 'posts.*': false })
111
111
  const results: boolean[] = []
112
112
 
113
113
  effect(() => {
114
- results.push(can("posts.read"))
114
+ results.push(can('posts.read'))
115
115
  })
116
116
 
117
- can.set({ "posts.*": true })
117
+ can.set({ 'posts.*': true })
118
118
  expect(results).toEqual([false, true])
119
119
  })
120
120
 
121
- it("wildcards are reactive after patch()", () => {
122
- const can = createPermissions({ "posts.*": true })
121
+ it('wildcards are reactive after patch()', () => {
122
+ const can = createPermissions({ 'posts.*': true })
123
123
  const results: boolean[] = []
124
124
 
125
125
  effect(() => {
126
- results.push(can("posts.read"))
126
+ results.push(can('posts.read'))
127
127
  })
128
128
 
129
- can.patch({ "posts.read": false }) // exact override
129
+ can.patch({ 'posts.read': false }) // exact override
130
130
  expect(results).toEqual([true, false])
131
131
  })
132
132
 
133
- it("removing wildcard via set() removes its effect", () => {
134
- const can = createPermissions({ "posts.*": true })
135
- expect(can("posts.read")).toBe(true)
133
+ it('removing wildcard via set() removes its effect', () => {
134
+ const can = createPermissions({ 'posts.*': true })
135
+ expect(can('posts.read')).toBe(true)
136
136
 
137
137
  can.set({}) // remove all permissions
138
- expect(can("posts.read")).toBe(false)
138
+ expect(can('posts.read')).toBe(false)
139
139
  })
140
140
  })
141
141
  })
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Computed } from "@pyreon/reactivity"
1
+ import type { Computed } from '@pyreon/reactivity'
2
2
 
3
3
  // ─── Permission values ───────────────────────────────────────────────────────
4
4