@pyreon/permissions 0.11.5 → 0.11.6

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,197 +1,197 @@
1
- import { computed, effect, signal } from "@pyreon/reactivity"
2
- import { describe, expect, it } from "vitest"
3
- import { createPermissions } from "../index"
1
+ import { computed, effect, signal } from '@pyreon/reactivity'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { createPermissions } from '../index'
4
4
 
5
- describe("createPermissions", () => {
5
+ describe('createPermissions', () => {
6
6
  // ─── Basic checks ────────────────────────────────────────────────────────
7
7
 
8
- describe("static permissions", () => {
9
- it("returns true for granted permissions", () => {
10
- const can = createPermissions({ "posts.read": true })
11
- expect(can("posts.read")).toBe(true)
8
+ describe('static permissions', () => {
9
+ it('returns true for granted permissions', () => {
10
+ const can = createPermissions({ 'posts.read': true })
11
+ expect(can('posts.read')).toBe(true)
12
12
  })
13
13
 
14
- it("returns false for denied permissions", () => {
15
- const can = createPermissions({ "posts.delete": false })
16
- expect(can("posts.delete")).toBe(false)
14
+ it('returns false for denied permissions', () => {
15
+ const can = createPermissions({ 'posts.delete': false })
16
+ expect(can('posts.delete')).toBe(false)
17
17
  })
18
18
 
19
- it("returns false for undefined permissions", () => {
20
- const can = createPermissions({ "posts.read": true })
21
- expect(can("users.manage")).toBe(false)
19
+ it('returns false for undefined permissions', () => {
20
+ const can = createPermissions({ 'posts.read': true })
21
+ expect(can('users.manage')).toBe(false)
22
22
  })
23
23
 
24
- it("handles empty initial permissions", () => {
24
+ it('handles empty initial permissions', () => {
25
25
  const can = createPermissions()
26
- expect(can("anything")).toBe(false)
26
+ expect(can('anything')).toBe(false)
27
27
  })
28
28
 
29
- it("handles empty object", () => {
29
+ it('handles empty object', () => {
30
30
  const can = createPermissions({})
31
- expect(can("anything")).toBe(false)
31
+ expect(can('anything')).toBe(false)
32
32
  })
33
33
  })
34
34
 
35
35
  // ─── Predicate permissions ─────────────────────────────────────────────
36
36
 
37
- describe("predicate permissions", () => {
38
- it("evaluates predicate without context", () => {
39
- const role = signal("admin")
37
+ describe('predicate permissions', () => {
38
+ it('evaluates predicate without context', () => {
39
+ const role = signal('admin')
40
40
  const can = createPermissions({
41
- "users.manage": () => role() === "admin",
41
+ 'users.manage': () => role() === 'admin',
42
42
  })
43
- expect(can("users.manage")).toBe(true)
43
+ expect(can('users.manage')).toBe(true)
44
44
  })
45
45
 
46
- it("evaluates predicate with context", () => {
47
- const userId = signal("user-1")
46
+ it('evaluates predicate with context', () => {
47
+ const userId = signal('user-1')
48
48
  const can = createPermissions({
49
- "posts.update": (post: any) => post?.authorId === userId.peek(),
49
+ 'posts.update': (post: any) => post?.authorId === userId.peek(),
50
50
  })
51
51
 
52
- expect(can("posts.update", { authorId: "user-1" })).toBe(true)
53
- expect(can("posts.update", { authorId: "user-2" })).toBe(false)
52
+ expect(can('posts.update', { authorId: 'user-1' })).toBe(true)
53
+ expect(can('posts.update', { authorId: 'user-2' })).toBe(false)
54
54
  })
55
55
 
56
- it("predicate without context returns result of calling with undefined", () => {
56
+ it('predicate without context returns result of calling with undefined', () => {
57
57
  const can = createPermissions({
58
- "posts.update": (post?: any) => post?.authorId === "user-1",
58
+ 'posts.update': (post?: any) => post?.authorId === 'user-1',
59
59
  })
60
60
  // No context — post is undefined, so authorId check fails
61
- expect(can("posts.update")).toBe(false)
61
+ expect(can('posts.update')).toBe(false)
62
62
  })
63
63
 
64
- it("predicate that ignores context", () => {
64
+ it('predicate that ignores context', () => {
65
65
  const can = createPermissions({
66
- "posts.create": () => true,
66
+ 'posts.create': () => true,
67
67
  })
68
- expect(can("posts.create")).toBe(true)
68
+ expect(can('posts.create')).toBe(true)
69
69
  })
70
70
  })
71
71
 
72
72
  // ─── Wildcard matching ─────────────────────────────────────────────────
73
73
 
74
- describe("wildcard matching", () => {
75
- it("matches prefix wildcard", () => {
76
- const can = createPermissions({ "posts.*": true })
77
- expect(can("posts.read")).toBe(true)
78
- expect(can("posts.create")).toBe(true)
79
- expect(can("posts.delete")).toBe(true)
74
+ describe('wildcard matching', () => {
75
+ it('matches prefix wildcard', () => {
76
+ const can = createPermissions({ 'posts.*': true })
77
+ expect(can('posts.read')).toBe(true)
78
+ expect(can('posts.create')).toBe(true)
79
+ expect(can('posts.delete')).toBe(true)
80
80
  })
81
81
 
82
- it("exact match takes precedence over wildcard", () => {
82
+ it('exact match takes precedence over wildcard', () => {
83
83
  const can = createPermissions({
84
- "posts.*": true,
85
- "posts.delete": false,
84
+ 'posts.*': true,
85
+ 'posts.delete': false,
86
86
  })
87
- expect(can("posts.read")).toBe(true)
88
- expect(can("posts.delete")).toBe(false)
87
+ expect(can('posts.read')).toBe(true)
88
+ expect(can('posts.delete')).toBe(false)
89
89
  })
90
90
 
91
- it("global wildcard matches everything", () => {
92
- const can = createPermissions({ "*": true })
93
- expect(can("posts.read")).toBe(true)
94
- expect(can("users.manage")).toBe(true)
95
- expect(can("anything")).toBe(true)
91
+ it('global wildcard matches everything', () => {
92
+ const can = createPermissions({ '*': true })
93
+ expect(can('posts.read')).toBe(true)
94
+ expect(can('users.manage')).toBe(true)
95
+ expect(can('anything')).toBe(true)
96
96
  })
97
97
 
98
- it("exact match takes precedence over global wildcard", () => {
98
+ it('exact match takes precedence over global wildcard', () => {
99
99
  const can = createPermissions({
100
- "*": true,
101
- "billing.export": false,
100
+ '*': true,
101
+ 'billing.export': false,
102
102
  })
103
- expect(can("posts.read")).toBe(true)
104
- expect(can("billing.export")).toBe(false)
103
+ expect(can('posts.read')).toBe(true)
104
+ expect(can('billing.export')).toBe(false)
105
105
  })
106
106
 
107
- it("prefix wildcard takes precedence over global wildcard", () => {
107
+ it('prefix wildcard takes precedence over global wildcard', () => {
108
108
  const can = createPermissions({
109
- "*": false,
110
- "posts.*": true,
109
+ '*': false,
110
+ 'posts.*': true,
111
111
  })
112
- expect(can("posts.read")).toBe(true)
113
- expect(can("users.manage")).toBe(false)
112
+ expect(can('posts.read')).toBe(true)
113
+ expect(can('users.manage')).toBe(false)
114
114
  })
115
115
 
116
- it("wildcard with predicate", () => {
116
+ it('wildcard with predicate', () => {
117
117
  const can = createPermissions({
118
- "posts.*": (post: any) => post?.status !== "archived",
118
+ 'posts.*': (post: any) => post?.status !== 'archived',
119
119
  })
120
- expect(can("posts.read", { status: "published" })).toBe(true)
121
- expect(can("posts.update", { status: "archived" })).toBe(false)
120
+ expect(can('posts.read', { status: 'published' })).toBe(true)
121
+ expect(can('posts.update', { status: 'archived' })).toBe(false)
122
122
  })
123
123
 
124
- it("does not match partial prefixes", () => {
125
- const can = createPermissions({ "post.*": true })
126
- expect(can("posts.read")).toBe(false) // 'posts' !== 'post'
124
+ it('does not match partial prefixes', () => {
125
+ const can = createPermissions({ 'post.*': true })
126
+ expect(can('posts.read')).toBe(false) // 'posts' !== 'post'
127
127
  })
128
128
  })
129
129
 
130
130
  // ─── can.not ───────────────────────────────────────────────────────────
131
131
 
132
- describe("can.not()", () => {
133
- it("returns inverse of can()", () => {
132
+ describe('can.not()', () => {
133
+ it('returns inverse of can()', () => {
134
134
  const can = createPermissions({
135
- "posts.read": true,
136
- "posts.delete": false,
135
+ 'posts.read': true,
136
+ 'posts.delete': false,
137
137
  })
138
- expect(can.not("posts.read")).toBe(false)
139
- expect(can.not("posts.delete")).toBe(true)
140
- expect(can.not("users.manage")).toBe(true)
138
+ expect(can.not('posts.read')).toBe(false)
139
+ expect(can.not('posts.delete')).toBe(true)
140
+ expect(can.not('users.manage')).toBe(true)
141
141
  })
142
142
 
143
- it("works with predicates and context", () => {
143
+ it('works with predicates and context', () => {
144
144
  const can = createPermissions({
145
- "posts.update": (post: any) => post?.authorId === "me",
145
+ 'posts.update': (post: any) => post?.authorId === 'me',
146
146
  })
147
- expect(can.not("posts.update", { authorId: "me" })).toBe(false)
148
- expect(can.not("posts.update", { authorId: "other" })).toBe(true)
147
+ expect(can.not('posts.update', { authorId: 'me' })).toBe(false)
148
+ expect(can.not('posts.update', { authorId: 'other' })).toBe(true)
149
149
  })
150
150
  })
151
151
 
152
152
  // ─── can.all / can.any ─────────────────────────────────────────────────
153
153
 
154
- describe("can.all()", () => {
155
- it("returns true when all permissions are granted", () => {
154
+ describe('can.all()', () => {
155
+ it('returns true when all permissions are granted', () => {
156
156
  const can = createPermissions({
157
- "posts.read": true,
158
- "posts.create": true,
157
+ 'posts.read': true,
158
+ 'posts.create': true,
159
159
  })
160
- expect(can.all("posts.read", "posts.create")).toBe(true)
160
+ expect(can.all('posts.read', 'posts.create')).toBe(true)
161
161
  })
162
162
 
163
- it("returns false when any permission is denied", () => {
163
+ it('returns false when any permission is denied', () => {
164
164
  const can = createPermissions({
165
- "posts.read": true,
166
- "posts.delete": false,
165
+ 'posts.read': true,
166
+ 'posts.delete': false,
167
167
  })
168
- expect(can.all("posts.read", "posts.delete")).toBe(false)
168
+ expect(can.all('posts.read', 'posts.delete')).toBe(false)
169
169
  })
170
170
 
171
- it("returns true for empty args", () => {
171
+ it('returns true for empty args', () => {
172
172
  const can = createPermissions()
173
173
  expect(can.all()).toBe(true)
174
174
  })
175
175
  })
176
176
 
177
- describe("can.any()", () => {
178
- it("returns true when any permission is granted", () => {
177
+ describe('can.any()', () => {
178
+ it('returns true when any permission is granted', () => {
179
179
  const can = createPermissions({
180
- "posts.read": false,
181
- "posts.create": true,
180
+ 'posts.read': false,
181
+ 'posts.create': true,
182
182
  })
183
- expect(can.any("posts.read", "posts.create")).toBe(true)
183
+ expect(can.any('posts.read', 'posts.create')).toBe(true)
184
184
  })
185
185
 
186
- it("returns false when no permissions are granted", () => {
186
+ it('returns false when no permissions are granted', () => {
187
187
  const can = createPermissions({
188
- "posts.read": false,
189
- "posts.delete": false,
188
+ 'posts.read': false,
189
+ 'posts.delete': false,
190
190
  })
191
- expect(can.any("posts.read", "posts.delete")).toBe(false)
191
+ expect(can.any('posts.read', 'posts.delete')).toBe(false)
192
192
  })
193
193
 
194
- it("returns false for empty args", () => {
194
+ it('returns false for empty args', () => {
195
195
  const can = createPermissions()
196
196
  expect(can.any()).toBe(false)
197
197
  })
@@ -199,83 +199,83 @@ describe("createPermissions", () => {
199
199
 
200
200
  // ─── set / patch ───────────────────────────────────────────────────────
201
201
 
202
- describe("set()", () => {
203
- it("replaces all permissions", () => {
202
+ describe('set()', () => {
203
+ it('replaces all permissions', () => {
204
204
  const can = createPermissions({
205
- "posts.read": true,
206
- "users.manage": true,
205
+ 'posts.read': true,
206
+ 'users.manage': true,
207
207
  })
208
- expect(can("posts.read")).toBe(true)
209
- expect(can("users.manage")).toBe(true)
208
+ expect(can('posts.read')).toBe(true)
209
+ expect(can('users.manage')).toBe(true)
210
210
 
211
- can.set({ "posts.read": false })
212
- expect(can("posts.read")).toBe(false)
213
- expect(can("users.manage")).toBe(false) // replaced, not merged
211
+ can.set({ 'posts.read': false })
212
+ expect(can('posts.read')).toBe(false)
213
+ expect(can('users.manage')).toBe(false) // replaced, not merged
214
214
  })
215
215
 
216
- it("triggers reactive updates", () => {
217
- const can = createPermissions({ "posts.read": true })
216
+ it('triggers reactive updates', () => {
217
+ const can = createPermissions({ 'posts.read': true })
218
218
  const results: boolean[] = []
219
219
 
220
220
  effect(() => {
221
- results.push(can("posts.read"))
221
+ results.push(can('posts.read'))
222
222
  })
223
223
 
224
224
  expect(results).toEqual([true])
225
225
 
226
- can.set({ "posts.read": false })
226
+ can.set({ 'posts.read': false })
227
227
  expect(results).toEqual([true, false])
228
228
  })
229
229
  })
230
230
 
231
- describe("patch()", () => {
232
- it("merges with existing permissions", () => {
231
+ describe('patch()', () => {
232
+ it('merges with existing permissions', () => {
233
233
  const can = createPermissions({
234
- "posts.read": true,
235
- "users.manage": false,
234
+ 'posts.read': true,
235
+ 'users.manage': false,
236
236
  })
237
237
 
238
- can.patch({ "users.manage": true, "billing.view": true })
239
- expect(can("posts.read")).toBe(true) // unchanged
240
- expect(can("users.manage")).toBe(true) // updated
241
- expect(can("billing.view")).toBe(true) // added
238
+ can.patch({ 'users.manage': true, 'billing.view': true })
239
+ expect(can('posts.read')).toBe(true) // unchanged
240
+ expect(can('users.manage')).toBe(true) // updated
241
+ expect(can('billing.view')).toBe(true) // added
242
242
  })
243
243
 
244
- it("triggers reactive updates", () => {
245
- const can = createPermissions({ "posts.read": false })
244
+ it('triggers reactive updates', () => {
245
+ const can = createPermissions({ 'posts.read': false })
246
246
  const results: boolean[] = []
247
247
 
248
248
  effect(() => {
249
- results.push(can("posts.read"))
249
+ results.push(can('posts.read'))
250
250
  })
251
251
 
252
252
  expect(results).toEqual([false])
253
253
 
254
- can.patch({ "posts.read": true })
254
+ can.patch({ 'posts.read': true })
255
255
  expect(results).toEqual([false, true])
256
256
  })
257
257
  })
258
258
 
259
259
  // ─── Reactivity ────────────────────────────────────────────────────────
260
260
 
261
- describe("reactivity", () => {
262
- it("can() is reactive inside effect", () => {
263
- const can = createPermissions({ "posts.read": true })
261
+ describe('reactivity', () => {
262
+ it('can() is reactive inside effect', () => {
263
+ const can = createPermissions({ 'posts.read': true })
264
264
  const results: boolean[] = []
265
265
 
266
266
  effect(() => {
267
- results.push(can("posts.read"))
267
+ results.push(can('posts.read'))
268
268
  })
269
269
 
270
- can.set({ "posts.read": false })
271
- can.set({ "posts.read": true })
270
+ can.set({ 'posts.read': false })
271
+ can.set({ 'posts.read': true })
272
272
 
273
273
  expect(results).toEqual([true, false, true])
274
274
  })
275
275
 
276
- it("can() is reactive inside computed", () => {
276
+ it('can() is reactive inside computed', () => {
277
277
  const can = createPermissions({ admin: true })
278
- const isAdmin = computed(() => can("admin"))
278
+ const isAdmin = computed(() => can('admin'))
279
279
 
280
280
  expect(isAdmin()).toBe(true)
281
281
 
@@ -283,61 +283,61 @@ describe("createPermissions", () => {
283
283
  expect(isAdmin()).toBe(false)
284
284
  })
285
285
 
286
- it("can.not() is reactive", () => {
287
- const can = createPermissions({ "posts.read": true })
286
+ it('can.not() is reactive', () => {
287
+ const can = createPermissions({ 'posts.read': true })
288
288
  const results: boolean[] = []
289
289
 
290
290
  effect(() => {
291
- results.push(can.not("posts.read"))
291
+ results.push(can.not('posts.read'))
292
292
  })
293
293
 
294
- can.set({ "posts.read": false })
294
+ can.set({ 'posts.read': false })
295
295
  expect(results).toEqual([false, true])
296
296
  })
297
297
 
298
- it("can.all() is reactive", () => {
298
+ it('can.all() is reactive', () => {
299
299
  const can = createPermissions({
300
- "posts.read": true,
301
- "posts.create": true,
300
+ 'posts.read': true,
301
+ 'posts.create': true,
302
302
  })
303
303
  const results: boolean[] = []
304
304
 
305
305
  effect(() => {
306
- results.push(can.all("posts.read", "posts.create"))
306
+ results.push(can.all('posts.read', 'posts.create'))
307
307
  })
308
308
 
309
- can.patch({ "posts.create": false })
309
+ can.patch({ 'posts.create': false })
310
310
  expect(results).toEqual([true, false])
311
311
  })
312
312
 
313
- it("can.any() is reactive", () => {
313
+ it('can.any() is reactive', () => {
314
314
  const can = createPermissions({
315
- "posts.read": false,
316
- "posts.create": true,
315
+ 'posts.read': false,
316
+ 'posts.create': true,
317
317
  })
318
318
  const results: boolean[] = []
319
319
 
320
320
  effect(() => {
321
- results.push(can.any("posts.read", "posts.create"))
321
+ results.push(can.any('posts.read', 'posts.create'))
322
322
  })
323
323
 
324
- can.patch({ "posts.create": false })
324
+ can.patch({ 'posts.create': false })
325
325
  expect(results).toEqual([true, false])
326
326
  })
327
327
 
328
- it("predicate with reactive signals inside", () => {
329
- const role = signal("admin")
328
+ it('predicate with reactive signals inside', () => {
329
+ const role = signal('admin')
330
330
  const can = createPermissions({
331
- "users.manage": () => role() === "admin",
331
+ 'users.manage': () => role() === 'admin',
332
332
  })
333
333
 
334
334
  // The predicate reads `role()` but reactivity is driven by
335
335
  // the permission version signal, not the inner signal.
336
336
  // To react to role changes, update permissions:
337
- expect(can("users.manage")).toBe(true)
337
+ expect(can('users.manage')).toBe(true)
338
338
 
339
- can.patch({ "users.manage": () => role() === "admin" })
340
- role.set("viewer")
339
+ can.patch({ 'users.manage': () => role() === 'admin' })
340
+ role.set('viewer')
341
341
  // Need to re-evaluate — the predicate itself reads the signal
342
342
  // but the permission system doesn't track inner signal deps.
343
343
  // This is by design: update permissions via set/patch when the source changes.
@@ -346,55 +346,55 @@ describe("createPermissions", () => {
346
346
 
347
347
  // ─── granted / entries ─────────────────────────────────────────────────
348
348
 
349
- describe("granted()", () => {
350
- it("returns keys with true values", () => {
349
+ describe('granted()', () => {
350
+ it('returns keys with true values', () => {
351
351
  const can = createPermissions({
352
- "posts.read": true,
353
- "posts.delete": false,
354
- "users.manage": true,
352
+ 'posts.read': true,
353
+ 'posts.delete': false,
354
+ 'users.manage': true,
355
355
  })
356
- expect(can.granted()).toEqual(expect.arrayContaining(["posts.read", "users.manage"]))
357
- expect(can.granted()).not.toContain("posts.delete")
356
+ expect(can.granted()).toEqual(expect.arrayContaining(['posts.read', 'users.manage']))
357
+ expect(can.granted()).not.toContain('posts.delete')
358
358
  })
359
359
 
360
- it("includes predicate keys", () => {
360
+ it('includes predicate keys', () => {
361
361
  const can = createPermissions({
362
- "posts.update": (post: any) => post?.authorId === "me",
362
+ 'posts.update': (post: any) => post?.authorId === 'me',
363
363
  })
364
364
  // Predicates are capabilities — they exist
365
- expect(can.granted()).toContain("posts.update")
365
+ expect(can.granted()).toContain('posts.update')
366
366
  })
367
367
 
368
- it("is reactive", () => {
369
- const can = createPermissions({ "posts.read": true })
368
+ it('is reactive', () => {
369
+ const can = createPermissions({ 'posts.read': true })
370
370
  const results: string[][] = []
371
371
 
372
372
  effect(() => {
373
373
  results.push([...can.granted()])
374
374
  })
375
375
 
376
- can.patch({ "users.manage": true })
377
- expect(results).toEqual([["posts.read"], ["posts.read", "users.manage"]])
376
+ can.patch({ 'users.manage': true })
377
+ expect(results).toEqual([['posts.read'], ['posts.read', 'users.manage']])
378
378
  })
379
379
  })
380
380
 
381
- describe("entries()", () => {
382
- it("returns all entries", () => {
381
+ describe('entries()', () => {
382
+ it('returns all entries', () => {
383
383
  const can = createPermissions({
384
- "posts.read": true,
385
- "posts.delete": false,
384
+ 'posts.read': true,
385
+ 'posts.delete': false,
386
386
  })
387
387
  const entries = can.entries()
388
388
  expect(entries).toHaveLength(2)
389
389
  expect(entries).toEqual(
390
390
  expect.arrayContaining([
391
- ["posts.read", true],
392
- ["posts.delete", false],
391
+ ['posts.read', true],
392
+ ['posts.delete', false],
393
393
  ]),
394
394
  )
395
395
  })
396
396
 
397
- it("is reactive", () => {
397
+ it('is reactive', () => {
398
398
  const can = createPermissions({ a: true })
399
399
  const counts: number[] = []
400
400
 
@@ -409,50 +409,50 @@ describe("createPermissions", () => {
409
409
 
410
410
  // ─── Error handling & edge cases ──────────────────────────────────────
411
411
 
412
- describe("predicate that throws", () => {
413
- it("returns false when predicate throws", () => {
412
+ describe('predicate that throws', () => {
413
+ it('returns false when predicate throws', () => {
414
414
  const can = createPermissions({
415
- "posts.update": () => {
416
- throw new Error("predicate boom")
415
+ 'posts.update': () => {
416
+ throw new Error('predicate boom')
417
417
  },
418
418
  })
419
419
  // Should not crash — returns false
420
- expect(can("posts.update")).toBe(false)
420
+ expect(can('posts.update')).toBe(false)
421
421
  })
422
422
 
423
- it("returns false with context when predicate throws", () => {
423
+ it('returns false with context when predicate throws', () => {
424
424
  const can = createPermissions({
425
- "posts.update": (_post: any) => {
426
- throw new Error("predicate boom")
425
+ 'posts.update': (_post: any) => {
426
+ throw new Error('predicate boom')
427
427
  },
428
428
  })
429
- expect(can("posts.update", { id: 1 })).toBe(false)
429
+ expect(can('posts.update', { id: 1 })).toBe(false)
430
430
  })
431
431
 
432
- it("can.not returns true when predicate throws", () => {
432
+ it('can.not returns true when predicate throws', () => {
433
433
  const can = createPermissions({
434
- "posts.update": () => {
435
- throw new Error("boom")
434
+ 'posts.update': () => {
435
+ throw new Error('boom')
436
436
  },
437
437
  })
438
- expect(can.not("posts.update")).toBe(true)
438
+ expect(can.not('posts.update')).toBe(true)
439
439
  })
440
440
  })
441
441
 
442
- describe("can.all/can.any with empty args", () => {
443
- it("can.all() with no args returns true (vacuous truth)", () => {
444
- const can = createPermissions({ "posts.read": false })
442
+ describe('can.all/can.any with empty args', () => {
443
+ it('can.all() with no args returns true (vacuous truth)', () => {
444
+ const can = createPermissions({ 'posts.read': false })
445
445
  expect(can.all()).toBe(true)
446
446
  })
447
447
 
448
- it("can.any() with no args returns false", () => {
449
- const can = createPermissions({ "posts.read": true })
448
+ it('can.any() with no args returns false', () => {
449
+ const can = createPermissions({ 'posts.read': true })
450
450
  expect(can.any()).toBe(false)
451
451
  })
452
452
  })
453
453
 
454
- describe("rapid set/patch cycles", () => {
455
- it("final state is correct after rapid set calls", () => {
454
+ describe('rapid set/patch cycles', () => {
455
+ it('final state is correct after rapid set calls', () => {
456
456
  const can = createPermissions({ a: true })
457
457
 
458
458
  for (let i = 0; i < 50; i++) {
@@ -460,10 +460,10 @@ describe("createPermissions", () => {
460
460
  }
461
461
 
462
462
  // 50 iterations, last i=49, 49 % 2 = 1, so false
463
- expect(can("a")).toBe(false)
463
+ expect(can('a')).toBe(false)
464
464
  })
465
465
 
466
- it("final state is correct after rapid patch calls", () => {
466
+ it('final state is correct after rapid patch calls', () => {
467
467
  const can = createPermissions({ a: true, b: false })
468
468
 
469
469
  for (let i = 0; i < 50; i++) {
@@ -471,16 +471,16 @@ describe("createPermissions", () => {
471
471
  }
472
472
 
473
473
  // last i=49: 49%2=1 → false, 49%3=1 → false
474
- expect(can("a")).toBe(false)
475
- expect(can("b")).toBe(false)
474
+ expect(can('a')).toBe(false)
475
+ expect(can('b')).toBe(false)
476
476
  })
477
477
 
478
- it("reactive effects see correct final state after rapid changes", () => {
478
+ it('reactive effects see correct final state after rapid changes', () => {
479
479
  const can = createPermissions({ a: true })
480
480
  const results: boolean[] = []
481
481
 
482
482
  effect(() => {
483
- results.push(can("a"))
483
+ results.push(can('a'))
484
484
  })
485
485
 
486
486
  can.set({ a: false })
@@ -492,7 +492,7 @@ describe("createPermissions", () => {
492
492
  expect(results[results.length - 1]).toBe(false)
493
493
  })
494
494
 
495
- it("interleaved set and patch produce correct state", () => {
495
+ it('interleaved set and patch produce correct state', () => {
496
496
  const can = createPermissions({})
497
497
 
498
498
  can.set({ a: true, b: true })
@@ -500,15 +500,15 @@ describe("createPermissions", () => {
500
500
  can.set({ a: false }) // replaces everything
501
501
  can.patch({ d: true })
502
502
 
503
- expect(can("a")).toBe(false)
504
- expect(can("b")).toBe(false) // cleared by set
505
- expect(can("c")).toBe(false) // cleared by set
506
- expect(can("d")).toBe(true) // added by last patch
503
+ expect(can('a')).toBe(false)
504
+ expect(can('b')).toBe(false) // cleared by set
505
+ expect(can('c')).toBe(false) // cleared by set
506
+ expect(can('d')).toBe(true) // added by last patch
507
507
  })
508
508
  })
509
509
 
510
- describe("cleanup / disposal", () => {
511
- it("granted computed still works after many set/patch cycles", () => {
510
+ describe('cleanup / disposal', () => {
511
+ it('granted computed still works after many set/patch cycles', () => {
512
512
  const can = createPermissions({ a: true })
513
513
 
514
514
  for (let i = 0; i < 20; i++) {
@@ -516,12 +516,12 @@ describe("createPermissions", () => {
516
516
  }
517
517
 
518
518
  const granted = can.granted()
519
- expect(granted).toContain("a")
520
- expect(granted).toContain("key-19")
521
- expect(granted).not.toContain("key-0") // cleared by last set
519
+ expect(granted).toContain('a')
520
+ expect(granted).toContain('key-19')
521
+ expect(granted).not.toContain('key-0') // cleared by last set
522
522
  })
523
523
 
524
- it("entries computed reflects current state after many mutations", () => {
524
+ it('entries computed reflects current state after many mutations', () => {
525
525
  const can = createPermissions({})
526
526
 
527
527
  can.set({ a: true })
@@ -529,15 +529,15 @@ describe("createPermissions", () => {
529
529
  can.set({ c: true })
530
530
 
531
531
  const entries = can.entries()
532
- expect(entries).toEqual([["c", true]])
532
+ expect(entries).toEqual([['c', true]])
533
533
  })
534
534
 
535
- it("effects tracking permissions react correctly to updates", () => {
535
+ it('effects tracking permissions react correctly to updates', () => {
536
536
  const can = createPermissions({ a: true })
537
537
  let count = 0
538
538
 
539
539
  effect(() => {
540
- can("a")
540
+ can('a')
541
541
  count++
542
542
  })
543
543
 
@@ -549,135 +549,135 @@ describe("createPermissions", () => {
549
549
 
550
550
  // ─── Real-world patterns ───────────────────────────────────────────────
551
551
 
552
- describe("real-world patterns", () => {
553
- it("role-based access control", () => {
552
+ describe('real-world patterns', () => {
553
+ it('role-based access control', () => {
554
554
  function fromRole(role: string) {
555
555
  const roles: Record<string, Record<string, boolean>> = {
556
- admin: { "posts.*": true, "users.*": true, "billing.*": true },
556
+ admin: { 'posts.*': true, 'users.*': true, 'billing.*': true },
557
557
  editor: {
558
- "posts.read": true,
559
- "posts.create": true,
560
- "posts.update": true,
561
- "users.read": true,
558
+ 'posts.read': true,
559
+ 'posts.create': true,
560
+ 'posts.update': true,
561
+ 'users.read': true,
562
562
  },
563
- viewer: { "posts.read": true },
563
+ viewer: { 'posts.read': true },
564
564
  }
565
565
  return roles[role] ?? {}
566
566
  }
567
567
 
568
- const can = createPermissions(fromRole("editor"))
568
+ const can = createPermissions(fromRole('editor'))
569
569
 
570
- expect(can("posts.read")).toBe(true)
571
- expect(can("posts.create")).toBe(true)
572
- expect(can("posts.delete")).toBe(false)
573
- expect(can("users.read")).toBe(true)
574
- expect(can("users.manage")).toBe(false)
575
- expect(can("billing.view")).toBe(false)
570
+ expect(can('posts.read')).toBe(true)
571
+ expect(can('posts.create')).toBe(true)
572
+ expect(can('posts.delete')).toBe(false)
573
+ expect(can('users.read')).toBe(true)
574
+ expect(can('users.manage')).toBe(false)
575
+ expect(can('billing.view')).toBe(false)
576
576
 
577
577
  // Promote to admin
578
- can.set(fromRole("admin"))
579
- expect(can("posts.delete")).toBe(true)
580
- expect(can("users.manage")).toBe(true)
581
- expect(can("billing.view")).toBe(true)
578
+ can.set(fromRole('admin'))
579
+ expect(can('posts.delete')).toBe(true)
580
+ expect(can('users.manage')).toBe(true)
581
+ expect(can('billing.view')).toBe(true)
582
582
  })
583
583
 
584
- it("feature flags mixed with permissions", () => {
584
+ it('feature flags mixed with permissions', () => {
585
585
  const can = createPermissions({
586
- "posts.read": true,
587
- "posts.create": true,
588
- "feature.new-editor": true,
589
- "feature.dark-mode": false,
590
- "tier.pro": true,
591
- "tier.enterprise": false,
586
+ 'posts.read': true,
587
+ 'posts.create': true,
588
+ 'feature.new-editor': true,
589
+ 'feature.dark-mode': false,
590
+ 'tier.pro': true,
591
+ 'tier.enterprise': false,
592
592
  })
593
593
 
594
- expect(can("feature.new-editor")).toBe(true)
595
- expect(can("feature.dark-mode")).toBe(false)
596
- expect(can("tier.pro")).toBe(true)
597
- expect(can("tier.enterprise")).toBe(false)
594
+ expect(can('feature.new-editor')).toBe(true)
595
+ expect(can('feature.dark-mode')).toBe(false)
596
+ expect(can('tier.pro')).toBe(true)
597
+ expect(can('tier.enterprise')).toBe(false)
598
598
  })
599
599
 
600
- it("instance-level ownership checks", () => {
601
- const currentUserId = "user-42"
600
+ it('instance-level ownership checks', () => {
601
+ const currentUserId = 'user-42'
602
602
 
603
603
  const can = createPermissions({
604
- "posts.read": true,
605
- "posts.update": (post: any) => post?.authorId === currentUserId,
606
- "posts.delete": (post: any) => post?.authorId === currentUserId && post?.status === "draft",
604
+ 'posts.read': true,
605
+ 'posts.update': (post: any) => post?.authorId === currentUserId,
606
+ 'posts.delete': (post: any) => post?.authorId === currentUserId && post?.status === 'draft',
607
607
  })
608
608
 
609
- const myPost = { authorId: "user-42", status: "draft" }
610
- const otherPost = { authorId: "user-99", status: "draft" }
611
- const publishedPost = { authorId: "user-42", status: "published" }
609
+ const myPost = { authorId: 'user-42', status: 'draft' }
610
+ const otherPost = { authorId: 'user-99', status: 'draft' }
611
+ const publishedPost = { authorId: 'user-42', status: 'published' }
612
612
 
613
- expect(can("posts.update", myPost)).toBe(true)
614
- expect(can("posts.update", otherPost)).toBe(false)
615
- expect(can("posts.delete", myPost)).toBe(true)
616
- expect(can("posts.delete", publishedPost)).toBe(false)
613
+ expect(can('posts.update', myPost)).toBe(true)
614
+ expect(can('posts.update', otherPost)).toBe(false)
615
+ expect(can('posts.delete', myPost)).toBe(true)
616
+ expect(can('posts.delete', publishedPost)).toBe(false)
617
617
  })
618
618
 
619
- it("multi-tenant with key prefixes", () => {
619
+ it('multi-tenant with key prefixes', () => {
620
620
  const can = createPermissions({
621
- "org:acme.admin": true,
622
- "ws:design.posts.*": true,
623
- "ws:engineering.posts.read": true,
621
+ 'org:acme.admin': true,
622
+ 'ws:design.posts.*': true,
623
+ 'ws:engineering.posts.read': true,
624
624
  })
625
625
 
626
- expect(can("org:acme.admin")).toBe(true)
627
- expect(can("ws:design.posts.read")).toBe(true)
628
- expect(can("ws:design.posts.delete")).toBe(true)
629
- expect(can("ws:engineering.posts.read")).toBe(true)
630
- expect(can("ws:engineering.posts.delete")).toBe(false)
626
+ expect(can('org:acme.admin')).toBe(true)
627
+ expect(can('ws:design.posts.read')).toBe(true)
628
+ expect(can('ws:design.posts.delete')).toBe(true)
629
+ expect(can('ws:engineering.posts.read')).toBe(true)
630
+ expect(can('ws:engineering.posts.delete')).toBe(false)
631
631
  })
632
632
 
633
- it("server response transformation", () => {
633
+ it('server response transformation', () => {
634
634
  // Simulated server response
635
635
  const serverResponse = {
636
- permissions: ["posts:read", "posts:create", "users:read"],
636
+ permissions: ['posts:read', 'posts:create', 'users:read'],
637
637
  }
638
638
 
639
639
  // Simple transform — not a framework feature, just a .map()
640
640
  const perms = Object.fromEntries(
641
- serverResponse.permissions.map((p) => [p.replace(":", "."), true]),
641
+ serverResponse.permissions.map((p) => [p.replace(':', '.'), true]),
642
642
  )
643
643
 
644
644
  const can = createPermissions(perms)
645
- expect(can("posts.read")).toBe(true)
646
- expect(can("posts.create")).toBe(true)
647
- expect(can("users.read")).toBe(true)
648
- expect(can("posts.delete")).toBe(false)
645
+ expect(can('posts.read')).toBe(true)
646
+ expect(can('posts.create')).toBe(true)
647
+ expect(can('users.read')).toBe(true)
648
+ expect(can('posts.delete')).toBe(false)
649
649
  })
650
650
 
651
- it("superadmin with global wildcard", () => {
652
- const can = createPermissions({ "*": true })
653
- expect(can("literally.anything")).toBe(true)
654
- expect(can("posts.read")).toBe(true)
655
- expect(can("admin.nuclear-launch")).toBe(true)
651
+ it('superadmin with global wildcard', () => {
652
+ const can = createPermissions({ '*': true })
653
+ expect(can('literally.anything')).toBe(true)
654
+ expect(can('posts.read')).toBe(true)
655
+ expect(can('admin.nuclear-launch')).toBe(true)
656
656
  })
657
657
 
658
- it("reactive role switching", () => {
658
+ it('reactive role switching', () => {
659
659
  function fromRole(role: string): Record<string, boolean> {
660
- if (role === "admin") return { "*": true }
661
- if (role === "editor") return { "posts.*": true, "users.read": true }
662
- return { "posts.read": true }
660
+ if (role === 'admin') return { '*': true }
661
+ if (role === 'editor') return { 'posts.*': true, 'users.read': true }
662
+ return { 'posts.read': true }
663
663
  }
664
664
 
665
- const can = createPermissions(fromRole("viewer"))
665
+ const can = createPermissions(fromRole('viewer'))
666
666
  const results: boolean[] = []
667
667
 
668
668
  effect(() => {
669
- results.push(can("posts.create"))
669
+ results.push(can('posts.create'))
670
670
  })
671
671
 
672
672
  expect(results).toEqual([false])
673
673
 
674
- can.set(fromRole("editor"))
674
+ can.set(fromRole('editor'))
675
675
  expect(results).toEqual([false, true])
676
676
 
677
- can.set(fromRole("admin"))
677
+ can.set(fromRole('admin'))
678
678
  expect(results).toEqual([false, true, true])
679
679
 
680
- can.set(fromRole("viewer"))
680
+ can.set(fromRole('viewer'))
681
681
  expect(results).toEqual([false, true, true, false])
682
682
  })
683
683
  })