@pyreon/permissions 0.9.0 → 0.11.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.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"3a01e622-1","name":"permissions.ts"},{"uid":"3a01e622-3","name":"context.ts"},{"uid":"3a01e622-5","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"3a01e622-1":{"renderedLength":2621,"gzipLength":1065,"brotliLength":0,"metaUid":"3a01e622-0"},"3a01e622-3":{"renderedLength":933,"gzipLength":457,"brotliLength":0,"metaUid":"3a01e622-2"},"3a01e622-5":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"3a01e622-4"}},"nodeMetas":{"3a01e622-0":{"id":"/src/permissions.ts","moduleParts":{"index.js":"3a01e622-1"},"imported":[{"uid":"3a01e622-6"}],"importedBy":[{"uid":"3a01e622-4"}]},"3a01e622-2":{"id":"/src/context.ts","moduleParts":{"index.js":"3a01e622-3"},"imported":[{"uid":"3a01e622-7"}],"importedBy":[{"uid":"3a01e622-4"}]},"3a01e622-4":{"id":"/src/index.ts","moduleParts":{"index.js":"3a01e622-5"},"imported":[{"uid":"3a01e622-0"},{"uid":"3a01e622-2"}],"importedBy":[],"isEntry":true},"3a01e622-6":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"3a01e622-0"}]},"3a01e622-7":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"3a01e622-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"54dceade-1","name":"context.ts"},{"uid":"54dceade-3","name":"permissions.ts"},{"uid":"54dceade-5","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"54dceade-1":{"renderedLength":933,"gzipLength":457,"brotliLength":0,"metaUid":"54dceade-0"},"54dceade-3":{"renderedLength":2767,"gzipLength":1113,"brotliLength":0,"metaUid":"54dceade-2"},"54dceade-5":{"renderedLength":0,"gzipLength":0,"brotliLength":0,"metaUid":"54dceade-4"}},"nodeMetas":{"54dceade-0":{"id":"/src/context.ts","moduleParts":{"index.js":"54dceade-1"},"imported":[{"uid":"54dceade-6"}],"importedBy":[{"uid":"54dceade-4"}]},"54dceade-2":{"id":"/src/permissions.ts","moduleParts":{"index.js":"54dceade-3"},"imported":[{"uid":"54dceade-7"}],"importedBy":[{"uid":"54dceade-4"}]},"54dceade-4":{"id":"/src/index.ts","moduleParts":{"index.js":"54dceade-5"},"imported":[{"uid":"54dceade-0"},{"uid":"54dceade-2"}],"importedBy":[],"isEntry":true},"54dceade-6":{"id":"@pyreon/core","moduleParts":{},"imported":[],"importedBy":[{"uid":"54dceade-0"}]},"54dceade-7":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"54dceade-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -1,22 +1,69 @@
1
- import { computed, signal } from "@pyreon/reactivity";
2
1
  import { createContext, provide, useContext } from "@pyreon/core";
2
+ import { computed, signal } from "@pyreon/reactivity";
3
3
 
4
+ //#region src/context.ts
5
+ const PermissionsContext = createContext(null);
6
+ /**
7
+ * Provide a permissions instance to descendant components.
8
+ * Use this for SSR isolation or testing — each request/test gets its own instance.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * const can = createPermissions({ ... })
13
+ *
14
+ * <PermissionsProvider instance={can}>
15
+ * <App />
16
+ * </PermissionsProvider>
17
+ * ```
18
+ */
19
+ function PermissionsProvider(props) {
20
+ provide(PermissionsContext, props.instance);
21
+ return props.children ?? null;
22
+ }
23
+ /**
24
+ * Access the nearest permissions instance from context.
25
+ * Must be used within a `<PermissionsProvider>`.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * const can = usePermissions()
30
+ * {() => can('posts.read') && <PostList />}
31
+ * ```
32
+ */
33
+ function usePermissions() {
34
+ const instance = useContext(PermissionsContext);
35
+ if (!instance) throw new Error("[@pyreon/permissions] usePermissions() must be used within <PermissionsProvider>.");
36
+ return instance;
37
+ }
38
+
39
+ //#endregion
4
40
  //#region src/permissions.ts
5
41
  /**
6
42
  * Resolve a permission key against the map.
7
43
  * Resolution order: exact match → wildcard (e.g., 'posts.*') → global wildcard ('*') → false.
8
44
  */
45
+ /**
46
+ * Safely evaluate a permission value. Predicates that throw are treated as denied.
47
+ */
48
+ function evaluate(value, context) {
49
+ if (typeof value === "function") try {
50
+ return value(context);
51
+ } catch {
52
+ return false;
53
+ }
54
+ return value;
55
+ }
9
56
  function resolve(map, key, context) {
10
57
  const exact = map.get(key);
11
- if (exact !== void 0) return typeof exact === "function" ? exact(context) : exact;
58
+ if (exact !== void 0) return evaluate(exact, context);
12
59
  const dotIndex = key.lastIndexOf(".");
13
60
  if (dotIndex !== -1) {
14
61
  const prefix = key.slice(0, dotIndex);
15
62
  const wildcard = map.get(`${prefix}.*`);
16
- if (wildcard !== void 0) return typeof wildcard === "function" ? wildcard(context) : wildcard;
63
+ if (wildcard !== void 0) return evaluate(wildcard, context);
17
64
  }
18
65
  const global = map.get("*");
19
- if (global !== void 0) return typeof global === "function" ? global(context) : global;
66
+ if (global !== void 0) return evaluate(global, context);
20
67
  return false;
21
68
  }
22
69
  /**
@@ -91,42 +138,6 @@ function createPermissions(initial) {
91
138
  return can;
92
139
  }
93
140
 
94
- //#endregion
95
- //#region src/context.ts
96
- const PermissionsContext = createContext(null);
97
- /**
98
- * Provide a permissions instance to descendant components.
99
- * Use this for SSR isolation or testing — each request/test gets its own instance.
100
- *
101
- * @example
102
- * ```tsx
103
- * const can = createPermissions({ ... })
104
- *
105
- * <PermissionsProvider instance={can}>
106
- * <App />
107
- * </PermissionsProvider>
108
- * ```
109
- */
110
- function PermissionsProvider(props) {
111
- provide(PermissionsContext, props.instance);
112
- return props.children ?? null;
113
- }
114
- /**
115
- * Access the nearest permissions instance from context.
116
- * Must be used within a `<PermissionsProvider>`.
117
- *
118
- * @example
119
- * ```tsx
120
- * const can = usePermissions()
121
- * {() => can('posts.read') && <PostList />}
122
- * ```
123
- */
124
- function usePermissions() {
125
- const instance = useContext(PermissionsContext);
126
- if (!instance) throw new Error("[@pyreon/permissions] usePermissions() must be used within <PermissionsProvider>.");
127
- return instance;
128
- }
129
-
130
141
  //#endregion
131
142
  export { PermissionsProvider, createPermissions, usePermissions };
132
143
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/permissions.ts","../src/context.ts"],"sourcesContent":["import { computed, signal } from '@pyreon/reactivity'\nimport type { PermissionMap, PermissionValue, Permissions } from './types'\n\n/**\n * Resolve a permission key against the map.\n * Resolution order: exact match → wildcard (e.g., 'posts.*') → global wildcard ('*') → false.\n */\nfunction resolve(\n map: Map<string, PermissionValue>,\n key: string,\n context?: unknown,\n): boolean {\n // 1. Exact match\n const exact = map.get(key)\n if (exact !== undefined) {\n return typeof exact === 'function' ? exact(context) : exact\n }\n\n // 2. Wildcard match — 'posts.read' matches 'posts.*'\n const dotIndex = key.lastIndexOf('.')\n if (dotIndex !== -1) {\n const prefix = key.slice(0, dotIndex)\n const wildcard = map.get(`${prefix}.*`)\n if (wildcard !== undefined) {\n return typeof wildcard === 'function' ? wildcard(context) : wildcard\n }\n }\n\n // 3. Global wildcard\n const global = map.get('*')\n if (global !== undefined) {\n return typeof global === 'function' ? global(context) : global\n }\n\n // 4. No match → denied\n return false\n}\n\n/**\n * Create a reactive permissions instance.\n *\n * The returned `can` function checks permissions reactively —\n * reads update automatically when permissions change via `set()` or `patch()`.\n *\n * @param initial - Optional initial permission map\n * @returns A callable `Permissions` instance\n *\n * @example\n * ```tsx\n * const can = createPermissions({\n * 'posts.read': true,\n * 'posts.update': (post: Post) => post.authorId === userId(),\n * 'users.manage': false,\n * })\n *\n * // Check (reactive in effects/computeds/JSX)\n * can('posts.read') // true\n * can('posts.update', myPost) // evaluates predicate\n *\n * // JSX\n * {() => can('posts.delete') && <DeleteButton />}\n *\n * // Update\n * can.set({ 'posts.read': true, 'admin': true })\n * can.patch({ 'users.manage': true })\n * ```\n */\nexport function createPermissions(initial?: PermissionMap): Permissions {\n // Internal reactive state — a signal holding the permission map\n const store = signal(toMap(initial))\n // Version counter — incremented on every set/patch to trigger reactive updates\n const version = signal(0)\n\n function toMap(obj?: PermissionMap): Map<string, PermissionValue> {\n if (!obj) return new Map()\n return new Map(Object.entries(obj))\n }\n\n // The main check function — reads `version` to subscribe in reactive contexts\n function can(key: string, context?: unknown): boolean {\n // Reading version subscribes this call to reactive updates\n version()\n return resolve(store.peek(), key, context)\n }\n\n can.not = (key: string, context?: unknown): boolean => {\n return !can(key, context)\n }\n\n can.all = (...keys: string[]): boolean => {\n return keys.every((key) => can(key))\n }\n\n can.any = (...keys: string[]): boolean => {\n return keys.some((key) => can(key))\n }\n\n can.set = (permissions: PermissionMap): void => {\n store.set(toMap(permissions))\n version.update((v) => v + 1)\n }\n\n can.patch = (permissions: PermissionMap): void => {\n const current = store.peek()\n for (const [key, value] of Object.entries(permissions)) {\n current.set(key, value)\n }\n store.set(current)\n version.update((v) => v + 1)\n }\n\n can.granted = computed(() => {\n version()\n const keys: string[] = []\n for (const [key, value] of store.peek()) {\n // Static true or predicate (capability exists)\n if (value === true || typeof value === 'function') {\n keys.push(key)\n }\n }\n return keys\n })\n\n can.entries = computed(() => {\n version()\n return [...store.peek().entries()]\n })\n\n return can as Permissions\n}\n","import { createContext, provide, useContext } from '@pyreon/core'\nimport type { VNodeChild } from '@pyreon/core'\nimport type { Permissions } from './types'\n\nconst PermissionsContext = createContext<Permissions | null>(null)\n\n/**\n * Provide a permissions instance to descendant components.\n * Use this for SSR isolation or testing — each request/test gets its own instance.\n *\n * @example\n * ```tsx\n * const can = createPermissions({ ... })\n *\n * <PermissionsProvider instance={can}>\n * <App />\n * </PermissionsProvider>\n * ```\n */\nexport function PermissionsProvider(props: {\n instance: Permissions\n children?: VNodeChild\n}): VNodeChild {\n provide(PermissionsContext, props.instance)\n\n return props.children ?? null\n}\n\n/**\n * Access the nearest permissions instance from context.\n * Must be used within a `<PermissionsProvider>`.\n *\n * @example\n * ```tsx\n * const can = usePermissions()\n * {() => can('posts.read') && <PostList />}\n * ```\n */\nexport function usePermissions(): Permissions {\n const instance = useContext(PermissionsContext)\n if (!instance) {\n throw new Error(\n '[@pyreon/permissions] usePermissions() must be used within <PermissionsProvider>.',\n )\n }\n return instance\n}\n"],"mappings":";;;;;;;;AAOA,SAAS,QACP,KACA,KACA,SACS;CAET,MAAM,QAAQ,IAAI,IAAI,IAAI;AAC1B,KAAI,UAAU,OACZ,QAAO,OAAO,UAAU,aAAa,MAAM,QAAQ,GAAG;CAIxD,MAAM,WAAW,IAAI,YAAY,IAAI;AACrC,KAAI,aAAa,IAAI;EACnB,MAAM,SAAS,IAAI,MAAM,GAAG,SAAS;EACrC,MAAM,WAAW,IAAI,IAAI,GAAG,OAAO,IAAI;AACvC,MAAI,aAAa,OACf,QAAO,OAAO,aAAa,aAAa,SAAS,QAAQ,GAAG;;CAKhE,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,KAAI,WAAW,OACb,QAAO,OAAO,WAAW,aAAa,OAAO,QAAQ,GAAG;AAI1D,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT,SAAgB,kBAAkB,SAAsC;CAEtE,MAAM,QAAQ,OAAO,MAAM,QAAQ,CAAC;CAEpC,MAAM,UAAU,OAAO,EAAE;CAEzB,SAAS,MAAM,KAAmD;AAChE,MAAI,CAAC,IAAK,wBAAO,IAAI,KAAK;AAC1B,SAAO,IAAI,IAAI,OAAO,QAAQ,IAAI,CAAC;;CAIrC,SAAS,IAAI,KAAa,SAA4B;AAEpD,WAAS;AACT,SAAO,QAAQ,MAAM,MAAM,EAAE,KAAK,QAAQ;;AAG5C,KAAI,OAAO,KAAa,YAA+B;AACrD,SAAO,CAAC,IAAI,KAAK,QAAQ;;AAG3B,KAAI,OAAO,GAAG,SAA4B;AACxC,SAAO,KAAK,OAAO,QAAQ,IAAI,IAAI,CAAC;;AAGtC,KAAI,OAAO,GAAG,SAA4B;AACxC,SAAO,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC;;AAGrC,KAAI,OAAO,gBAAqC;AAC9C,QAAM,IAAI,MAAM,YAAY,CAAC;AAC7B,UAAQ,QAAQ,MAAM,IAAI,EAAE;;AAG9B,KAAI,SAAS,gBAAqC;EAChD,MAAM,UAAU,MAAM,MAAM;AAC5B,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,YAAY,CACpD,SAAQ,IAAI,KAAK,MAAM;AAEzB,QAAM,IAAI,QAAQ;AAClB,UAAQ,QAAQ,MAAM,IAAI,EAAE;;AAG9B,KAAI,UAAU,eAAe;AAC3B,WAAS;EACT,MAAM,OAAiB,EAAE;AACzB,OAAK,MAAM,CAAC,KAAK,UAAU,MAAM,MAAM,CAErC,KAAI,UAAU,QAAQ,OAAO,UAAU,WACrC,MAAK,KAAK,IAAI;AAGlB,SAAO;GACP;AAEF,KAAI,UAAU,eAAe;AAC3B,WAAS;AACT,SAAO,CAAC,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC;GAClC;AAEF,QAAO;;;;;AC5HT,MAAM,qBAAqB,cAAkC,KAAK;;;;;;;;;;;;;;AAelE,SAAgB,oBAAoB,OAGrB;AACb,SAAQ,oBAAoB,MAAM,SAAS;AAE3C,QAAO,MAAM,YAAY;;;;;;;;;;;;AAa3B,SAAgB,iBAA8B;CAC5C,MAAM,WAAW,WAAW,mBAAmB;AAC/C,KAAI,CAAC,SACH,OAAM,IAAI,MACR,oFACD;AAEH,QAAO"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/context.ts","../src/permissions.ts"],"sourcesContent":["import type { VNodeChild } from \"@pyreon/core\"\nimport { createContext, provide, useContext } from \"@pyreon/core\"\nimport type { Permissions } from \"./types\"\n\nconst PermissionsContext = createContext<Permissions | null>(null)\n\n/**\n * Provide a permissions instance to descendant components.\n * Use this for SSR isolation or testing — each request/test gets its own instance.\n *\n * @example\n * ```tsx\n * const can = createPermissions({ ... })\n *\n * <PermissionsProvider instance={can}>\n * <App />\n * </PermissionsProvider>\n * ```\n */\nexport function PermissionsProvider(props: {\n instance: Permissions\n children?: VNodeChild\n}): VNodeChild {\n provide(PermissionsContext, props.instance)\n\n return props.children ?? null\n}\n\n/**\n * Access the nearest permissions instance from context.\n * Must be used within a `<PermissionsProvider>`.\n *\n * @example\n * ```tsx\n * const can = usePermissions()\n * {() => can('posts.read') && <PostList />}\n * ```\n */\nexport function usePermissions(): Permissions {\n const instance = useContext(PermissionsContext)\n if (!instance) {\n throw new Error(\n \"[@pyreon/permissions] usePermissions() must be used within <PermissionsProvider>.\",\n )\n }\n return instance\n}\n","import { computed, signal } from \"@pyreon/reactivity\"\nimport type { PermissionMap, Permissions, PermissionValue } from \"./types\"\n\n/**\n * Resolve a permission key against the map.\n * Resolution order: exact match → wildcard (e.g., 'posts.*') → global wildcard ('*') → false.\n */\n/**\n * Safely evaluate a permission value. Predicates that throw are treated as denied.\n */\nfunction evaluate(value: PermissionValue, context?: unknown): boolean {\n if (typeof value === \"function\") {\n try {\n return value(context)\n } catch {\n return false\n }\n }\n return value\n}\n\nfunction resolve(map: Map<string, PermissionValue>, key: string, context?: unknown): boolean {\n // 1. Exact match\n const exact = map.get(key)\n if (exact !== undefined) {\n return evaluate(exact, context)\n }\n\n // 2. Wildcard match — 'posts.read' matches 'posts.*'\n const dotIndex = key.lastIndexOf(\".\")\n if (dotIndex !== -1) {\n const prefix = key.slice(0, dotIndex)\n const wildcard = map.get(`${prefix}.*`)\n if (wildcard !== undefined) {\n return evaluate(wildcard, context)\n }\n }\n\n // 3. Global wildcard\n const global = map.get(\"*\")\n if (global !== undefined) {\n return evaluate(global, context)\n }\n\n // 4. No match → denied\n return false\n}\n\n/**\n * Create a reactive permissions instance.\n *\n * The returned `can` function checks permissions reactively —\n * reads update automatically when permissions change via `set()` or `patch()`.\n *\n * @param initial - Optional initial permission map\n * @returns A callable `Permissions` instance\n *\n * @example\n * ```tsx\n * const can = createPermissions({\n * 'posts.read': true,\n * 'posts.update': (post: Post) => post.authorId === userId(),\n * 'users.manage': false,\n * })\n *\n * // Check (reactive in effects/computeds/JSX)\n * can('posts.read') // true\n * can('posts.update', myPost) // evaluates predicate\n *\n * // JSX\n * {() => can('posts.delete') && <DeleteButton />}\n *\n * // Update\n * can.set({ 'posts.read': true, 'admin': true })\n * can.patch({ 'users.manage': true })\n * ```\n */\nexport function createPermissions(initial?: PermissionMap): Permissions {\n // Internal reactive state — a signal holding the permission map\n const store = signal(toMap(initial))\n // Version counter — incremented on every set/patch to trigger reactive updates\n const version = signal(0)\n\n function toMap(obj?: PermissionMap): Map<string, PermissionValue> {\n if (!obj) return new Map()\n return new Map(Object.entries(obj))\n }\n\n // The main check function — reads `version` to subscribe in reactive contexts\n function can(key: string, context?: unknown): boolean {\n // Reading version subscribes this call to reactive updates\n version()\n return resolve(store.peek(), key, context)\n }\n\n can.not = (key: string, context?: unknown): boolean => {\n return !can(key, context)\n }\n\n can.all = (...keys: string[]): boolean => {\n return keys.every((key) => can(key))\n }\n\n can.any = (...keys: string[]): boolean => {\n return keys.some((key) => can(key))\n }\n\n can.set = (permissions: PermissionMap): void => {\n store.set(toMap(permissions))\n version.update((v) => v + 1)\n }\n\n can.patch = (permissions: PermissionMap): void => {\n const current = store.peek()\n for (const [key, value] of Object.entries(permissions)) {\n current.set(key, value)\n }\n store.set(current)\n version.update((v) => v + 1)\n }\n\n can.granted = computed(() => {\n version()\n const keys: string[] = []\n for (const [key, value] of store.peek()) {\n // Static true or predicate (capability exists)\n if (value === true || typeof value === \"function\") {\n keys.push(key)\n }\n }\n return keys\n })\n\n can.entries = computed(() => {\n version()\n return [...store.peek().entries()]\n })\n\n return can as Permissions\n}\n"],"mappings":";;;;AAIA,MAAM,qBAAqB,cAAkC,KAAK;;;;;;;;;;;;;;AAelE,SAAgB,oBAAoB,OAGrB;AACb,SAAQ,oBAAoB,MAAM,SAAS;AAE3C,QAAO,MAAM,YAAY;;;;;;;;;;;;AAa3B,SAAgB,iBAA8B;CAC5C,MAAM,WAAW,WAAW,mBAAmB;AAC/C,KAAI,CAAC,SACH,OAAM,IAAI,MACR,oFACD;AAEH,QAAO;;;;;;;;;;;;ACnCT,SAAS,SAAS,OAAwB,SAA4B;AACpE,KAAI,OAAO,UAAU,WACnB,KAAI;AACF,SAAO,MAAM,QAAQ;SACf;AACN,SAAO;;AAGX,QAAO;;AAGT,SAAS,QAAQ,KAAmC,KAAa,SAA4B;CAE3F,MAAM,QAAQ,IAAI,IAAI,IAAI;AAC1B,KAAI,UAAU,OACZ,QAAO,SAAS,OAAO,QAAQ;CAIjC,MAAM,WAAW,IAAI,YAAY,IAAI;AACrC,KAAI,aAAa,IAAI;EACnB,MAAM,SAAS,IAAI,MAAM,GAAG,SAAS;EACrC,MAAM,WAAW,IAAI,IAAI,GAAG,OAAO,IAAI;AACvC,MAAI,aAAa,OACf,QAAO,SAAS,UAAU,QAAQ;;CAKtC,MAAM,SAAS,IAAI,IAAI,IAAI;AAC3B,KAAI,WAAW,OACb,QAAO,SAAS,QAAQ,QAAQ;AAIlC,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCT,SAAgB,kBAAkB,SAAsC;CAEtE,MAAM,QAAQ,OAAO,MAAM,QAAQ,CAAC;CAEpC,MAAM,UAAU,OAAO,EAAE;CAEzB,SAAS,MAAM,KAAmD;AAChE,MAAI,CAAC,IAAK,wBAAO,IAAI,KAAK;AAC1B,SAAO,IAAI,IAAI,OAAO,QAAQ,IAAI,CAAC;;CAIrC,SAAS,IAAI,KAAa,SAA4B;AAEpD,WAAS;AACT,SAAO,QAAQ,MAAM,MAAM,EAAE,KAAK,QAAQ;;AAG5C,KAAI,OAAO,KAAa,YAA+B;AACrD,SAAO,CAAC,IAAI,KAAK,QAAQ;;AAG3B,KAAI,OAAO,GAAG,SAA4B;AACxC,SAAO,KAAK,OAAO,QAAQ,IAAI,IAAI,CAAC;;AAGtC,KAAI,OAAO,GAAG,SAA4B;AACxC,SAAO,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC;;AAGrC,KAAI,OAAO,gBAAqC;AAC9C,QAAM,IAAI,MAAM,YAAY,CAAC;AAC7B,UAAQ,QAAQ,MAAM,IAAI,EAAE;;AAG9B,KAAI,SAAS,gBAAqC;EAChD,MAAM,UAAU,MAAM,MAAM;AAC5B,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,YAAY,CACpD,SAAQ,IAAI,KAAK,MAAM;AAEzB,QAAM,IAAI,QAAQ;AAClB,UAAQ,QAAQ,MAAM,IAAI,EAAE;;AAG9B,KAAI,UAAU,eAAe;AAC3B,WAAS;EACT,MAAM,OAAiB,EAAE;AACzB,OAAK,MAAM,CAAC,KAAK,UAAU,MAAM,MAAM,CAErC,KAAI,UAAU,QAAQ,OAAO,UAAU,WACrC,MAAK,KAAK,IAAI;AAGlB,SAAO;GACP;AAEF,KAAI,UAAU,eAAe;AAC3B,WAAS;AACT,SAAO,CAAC,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC;GAClC;AAEF,QAAO"}
@@ -1,5 +1,5 @@
1
- import { Computed } from "@pyreon/reactivity";
2
1
  import { VNodeChild } from "@pyreon/core";
2
+ import { Computed } from "@pyreon/reactivity";
3
3
 
4
4
  //#region src/types.d.ts
5
5
  /**
@@ -103,6 +103,36 @@ interface Permissions {
103
103
  entries: Computed<[string, PermissionValue][]>;
104
104
  }
105
105
  //#endregion
106
+ //#region src/context.d.ts
107
+ /**
108
+ * Provide a permissions instance to descendant components.
109
+ * Use this for SSR isolation or testing — each request/test gets its own instance.
110
+ *
111
+ * @example
112
+ * ```tsx
113
+ * const can = createPermissions({ ... })
114
+ *
115
+ * <PermissionsProvider instance={can}>
116
+ * <App />
117
+ * </PermissionsProvider>
118
+ * ```
119
+ */
120
+ declare function PermissionsProvider(props: {
121
+ instance: Permissions;
122
+ children?: VNodeChild;
123
+ }): VNodeChild;
124
+ /**
125
+ * Access the nearest permissions instance from context.
126
+ * Must be used within a `<PermissionsProvider>`.
127
+ *
128
+ * @example
129
+ * ```tsx
130
+ * const can = usePermissions()
131
+ * {() => can('posts.read') && <PostList />}
132
+ * ```
133
+ */
134
+ declare function usePermissions(): Permissions;
135
+ //#endregion
106
136
  //#region src/permissions.d.ts
107
137
  /**
108
138
  * Create a reactive permissions instance.
@@ -135,35 +165,5 @@ interface Permissions {
135
165
  */
136
166
  declare function createPermissions(initial?: PermissionMap): Permissions;
137
167
  //#endregion
138
- //#region src/context.d.ts
139
- /**
140
- * Provide a permissions instance to descendant components.
141
- * Use this for SSR isolation or testing — each request/test gets its own instance.
142
- *
143
- * @example
144
- * ```tsx
145
- * const can = createPermissions({ ... })
146
- *
147
- * <PermissionsProvider instance={can}>
148
- * <App />
149
- * </PermissionsProvider>
150
- * ```
151
- */
152
- declare function PermissionsProvider(props: {
153
- instance: Permissions;
154
- children?: VNodeChild;
155
- }): VNodeChild;
156
- /**
157
- * Access the nearest permissions instance from context.
158
- * Must be used within a `<PermissionsProvider>`.
159
- *
160
- * @example
161
- * ```tsx
162
- * const can = usePermissions()
163
- * {() => can('posts.read') && <PostList />}
164
- * ```
165
- */
166
- declare function usePermissions(): Permissions;
167
- //#endregion
168
168
  export { type PermissionMap, type PermissionPredicate, type PermissionValue, type Permissions, PermissionsProvider, createPermissions, usePermissions };
169
169
  //# sourceMappingURL=index2.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/permissions.ts","../../../src/context.ts"],"mappings":";;;;;;;AAQA;KAAY,mBAAA,wBACV,OAAA,GAAU,QAAA;;;;;;KAQA,eAAA,iCAER,mBAAA,CAAoB,QAAA;;AAFxB;;;;KAWY,aAAA,GAAgB,MAAA,SAAe,eAAA;;;;;UAQ1B,WAAA;EARQ;;;;AAQzB;;;;;;;;;;EARyB,CAuBtB,GAAA,UAAa,OAAA;EAAA;;;;;;;;EAUd,GAAA,GAAM,GAAA,UAAa,OAAA;EA8BA;;;;;;;;EApBnB,GAAA,MAAS,IAAA;EAiDkB;;;;;;ACrD7B;;EDcE,GAAA,MAAS,IAAA;ECd4D;;;;;;;;EDwBrE,GAAA,GAAM,WAAA,EAAa,aAAA;EExEL;;;;;;;;;EFmFd,KAAA,GAAQ,WAAA,EAAa,aAAA;EEjFrB;;;;;;AAiBF;;;;EF4EE,OAAA,EAAS,QAAA;;;;;EAMT,OAAA,EAAS,QAAA,UAAkB,eAAA;AAAA;;;;;;AAhH7B;;;;;;;;;AASA;;;;;;;;;AAWA;;;;;AAQA;;;iBC+BgB,iBAAA,CAAkB,OAAA,GAAU,aAAA,GAAgB,WAAA;;;;;AD3D5D;;;;;;;;;AASA;;iBEEgB,mBAAA,CAAoB,KAAA;EAClC,QAAA,EAAU,WAAA;EACV,QAAA,GAAW,UAAA;AAAA,IACT,UAAA;;;;AFMJ;;;;;AAQA;;iBEEgB,cAAA,CAAA,GAAkB,WAAA"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/types.ts","../../../src/context.ts","../../../src/permissions.ts"],"mappings":";;;;;;;AAQA;KAAY,mBAAA,wBAA2C,OAAA,GAAU,QAAA;;;;;;KAOrD,eAAA,iCAAgD,mBAAA,CAAoB,QAAA;;AAAhF;;;;KASY,aAAA,GAAgB,MAAA,SAAe,eAAA;;;;;UAQ1B,WAAA;EARQ;;;;AAQzB;;;;;;;;;;EARyB,CAuBtB,GAAA,UAAa,OAAA;EAAA;;;;;;;;EAUd,GAAA,GAAM,GAAA,UAAa,OAAA;EA8BA;;;;;;;;EApBnB,GAAA,MAAS,IAAA;EAiDkB;;;;;;ACjG7B;;ED0DE,GAAA,MAAS,IAAA;ECzDC;;;;;;;;EDmEV,GAAA,GAAM,WAAA,EAAa,aAAA;ECpEe;;;;AAmBpC;;;;;ED4DE,KAAA,GAAQ,WAAA,EAAa,aAAA;;;AErBvB;;;;;;;;EFiCE,OAAA,EAAS,QAAA;;;;;EAMT,OAAA,EAAS,QAAA,UAAkB,eAAA;AAAA;;;;;AA5G7B;;;;;;;;;AAOA;;iBCIgB,mBAAA,CAAoB,KAAA;EAClC,QAAA,EAAU,WAAA;EACV,QAAA,GAAW,UAAA;AAAA,IACT,UAAA;;;;ADEJ;;;;;AAQA;;iBCMgB,cAAA,CAAA,GAAkB,WAAA;;;;;;AD9BlC;;;;;;;;;AAOA;;;;;;;;;AASA;;;;;AAQA;;;iBE6CgB,iBAAA,CAAkB,OAAA,GAAU,aAAA,GAAgB,WAAA"}
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@pyreon/permissions",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Reactive permissions for Pyreon — type-safe, signal-driven, universal",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/pyreon/fundamentals.git",
9
- "directory": "packages/permissions"
8
+ "url": "https://github.com/pyreon/pyreon.git",
9
+ "directory": "packages/fundamentals/permissions"
10
10
  },
11
11
  "homepage": "https://github.com/pyreon/fundamentals/tree/main/packages/permissions#readme",
12
12
  "bugs": {
13
- "url": "https://github.com/pyreon/fundamentals/issues"
13
+ "url": "https://github.com/pyreon/pyreon/issues"
14
14
  },
15
15
  "publishConfig": {
16
16
  "access": "public"
@@ -37,10 +37,17 @@
37
37
  "build": "vl_rolldown_build",
38
38
  "dev": "vl_rolldown_build-watch",
39
39
  "test": "vitest run",
40
- "typecheck": "tsc --noEmit"
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "biome check ."
41
42
  },
42
43
  "peerDependencies": {
43
- "@pyreon/core": ">=0.7.0 <0.8.0",
44
- "@pyreon/reactivity": ">=0.7.0 <0.8.0"
44
+ "@pyreon/core": "^0.11.0",
45
+ "@pyreon/reactivity": "^0.11.0"
46
+ },
47
+ "devDependencies": {
48
+ "@happy-dom/global-registrator": "^20.8.3",
49
+ "@pyreon/core": "^0.11.0",
50
+ "@pyreon/reactivity": "^0.11.0",
51
+ "@vitus-labs/tools-lint": "^1.11.0"
45
52
  }
46
53
  }
package/src/context.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { createContext, provide, useContext } from '@pyreon/core'
2
- import type { VNodeChild } from '@pyreon/core'
3
- import type { Permissions } from './types'
1
+ import type { VNodeChild } from "@pyreon/core"
2
+ import { createContext, provide, useContext } from "@pyreon/core"
3
+ import type { Permissions } from "./types"
4
4
 
5
5
  const PermissionsContext = createContext<Permissions | null>(null)
6
6
 
@@ -40,7 +40,7 @@ export function usePermissions(): Permissions {
40
40
  const instance = useContext(PermissionsContext)
41
41
  if (!instance) {
42
42
  throw new Error(
43
- '[@pyreon/permissions] usePermissions() must be used within <PermissionsProvider>.',
43
+ "[@pyreon/permissions] usePermissions() must be used within <PermissionsProvider>.",
44
44
  )
45
45
  }
46
46
  return instance
package/src/index.ts CHANGED
@@ -23,13 +23,13 @@
23
23
  * ```
24
24
  */
25
25
 
26
- export { createPermissions } from './permissions'
27
- export { PermissionsProvider, usePermissions } from './context'
26
+ export { PermissionsProvider, usePermissions } from "./context"
27
+ export { createPermissions } from "./permissions"
28
28
 
29
29
  // Types
30
30
  export type {
31
31
  PermissionMap,
32
32
  PermissionPredicate,
33
- PermissionValue,
34
33
  Permissions,
35
- } from './types'
34
+ PermissionValue,
35
+ } from "./types"
@@ -1,35 +1,45 @@
1
- import { computed, signal } from '@pyreon/reactivity'
2
- import type { PermissionMap, PermissionValue, Permissions } from './types'
1
+ import { computed, signal } from "@pyreon/reactivity"
2
+ import type { PermissionMap, Permissions, PermissionValue } from "./types"
3
3
 
4
4
  /**
5
5
  * Resolve a permission key against the map.
6
6
  * Resolution order: exact match → wildcard (e.g., 'posts.*') → global wildcard ('*') → false.
7
7
  */
8
- function resolve(
9
- map: Map<string, PermissionValue>,
10
- key: string,
11
- context?: unknown,
12
- ): boolean {
8
+ /**
9
+ * Safely evaluate a permission value. Predicates that throw are treated as denied.
10
+ */
11
+ function evaluate(value: PermissionValue, context?: unknown): boolean {
12
+ if (typeof value === "function") {
13
+ try {
14
+ return value(context)
15
+ } catch {
16
+ return false
17
+ }
18
+ }
19
+ return value
20
+ }
21
+
22
+ function resolve(map: Map<string, PermissionValue>, key: string, context?: unknown): boolean {
13
23
  // 1. Exact match
14
24
  const exact = map.get(key)
15
25
  if (exact !== undefined) {
16
- return typeof exact === 'function' ? exact(context) : exact
26
+ return evaluate(exact, context)
17
27
  }
18
28
 
19
29
  // 2. Wildcard match — 'posts.read' matches 'posts.*'
20
- const dotIndex = key.lastIndexOf('.')
30
+ const dotIndex = key.lastIndexOf(".")
21
31
  if (dotIndex !== -1) {
22
32
  const prefix = key.slice(0, dotIndex)
23
33
  const wildcard = map.get(`${prefix}.*`)
24
34
  if (wildcard !== undefined) {
25
- return typeof wildcard === 'function' ? wildcard(context) : wildcard
35
+ return evaluate(wildcard, context)
26
36
  }
27
37
  }
28
38
 
29
39
  // 3. Global wildcard
30
- const global = map.get('*')
40
+ const global = map.get("*")
31
41
  if (global !== undefined) {
32
- return typeof global === 'function' ? global(context) : global
42
+ return evaluate(global, context)
33
43
  }
34
44
 
35
45
  // 4. No match → denied
@@ -114,7 +124,7 @@ export function createPermissions(initial?: PermissionMap): Permissions {
114
124
  const keys: string[] = []
115
125
  for (const [key, value] of store.peek()) {
116
126
  // Static true or predicate (capability exists)
117
- if (value === true || typeof value === 'function') {
127
+ if (value === true || typeof value === "function") {
118
128
  keys.push(key)
119
129
  }
120
130
  }