@vielzeug/ward 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -1,81 +1,75 @@
1
1
  # @vielzeug/ward
2
2
 
3
- > Minimal authorization engine with deterministic precedence, wildcard support, and runtime predicates.
4
-
5
- [![npm version](https://img.shields.io/npm/v/@vielzeug/ward)](https://www.npmjs.com/package/@vielzeug/ward) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
-
7
- <details>
8
- <summary>Quick Reference</summary>
9
-
10
- **Package:** `@vielzeug/ward` &nbsp;·&nbsp; **Category:** Auth
11
-
12
- **Key exports:** `createWard`, `allow`, `deny`, `predicate`, `owns`, `matchesPattern`, `patternCovers`, `guardRequest`, `guardRequestWith`, `WardPredicateError`, `WILDCARD`, `ANONYMOUS`
13
-
14
- **When to use:** Minimal authorization engine with deterministic precedence, wildcard support, and runtime predicates.
15
-
16
- **Related:** [@vielzeug/rune](https://vielzeug.dev/rune/) · [@vielzeug/wayfinder](https://vielzeug.dev/wayfinder/) · [@vielzeug/conduit](https://vielzeug.dev/conduit/)
17
-
18
- </details>
19
-
20
- `@vielzeug/ward` is part of Vielzeug and ships as a zero-dependency TypeScript package with ESM+CJS output.
3
+ Minimal authorization engine with deterministic precedence, wildcard support, and runtime predicates.
21
4
 
22
5
  ## Installation
23
6
 
24
7
  ```sh
25
8
  pnpm add @vielzeug/ward
26
- npm install @vielzeug/ward
27
- yarn add @vielzeug/ward
28
9
  ```
29
10
 
30
11
  ## Quick Start
31
12
 
32
13
  ```ts
33
- import { ANONYMOUS, WILDCARD, allow, createWard, deny, predicate } from '@vielzeug/ward';
14
+ import { ANONYMOUS, WILDCARD, allow, createWard, deny, owns } from '@vielzeug/ward';
34
15
 
35
16
  const ward = createWard<'read' | 'update', { authorId: string }>([
36
- // Multi-role rule: viewer and editor can both read
37
- ...allow(['viewer', 'editor'], 'posts', ['read']),
38
- // Editor can update their own posts (ownership predicate)
39
- ...allow('editor', 'posts', ['update'], { when: predicate.owns('authorId') }),
40
- // High-priority deny overrides any allow rule for blocked principals
17
+ ...allow([ANONYMOUS, 'viewer'], 'posts', ['read']),
18
+ ...allow('editor', 'posts', ['update'], { when: owns('authorId') }),
41
19
  ...deny('blocked', WILDCARD, [WILDCARD], { priority: 100 }),
42
- // Anonymous visitors can read posts
43
- ...allow(ANONYMOUS, 'posts', ['read']),
44
20
  ]);
45
21
 
46
22
  const principal = { id: 'u1', roles: ['editor'] };
47
23
 
48
- // Full decision object — narrow on .allowed for type-safe access
49
- const decision = ward.explain(principal, 'posts', 'update', { authorId: 'u2' });
50
- if (!decision.allowed) console.log(decision.reason); // 'no-matching-rule' | 'explicit-deny'
24
+ const decision = ward.explain({
25
+ principal,
26
+ resource: 'posts',
27
+ action: 'update',
28
+ data: { authorId: 'u2' },
29
+ });
51
30
 
52
- // Batch decisions across multiple resources/actions in one call
53
- const results = ward.checkAll(principal, [
31
+ const batch = ward.checkAll(principal, [
54
32
  { resource: 'posts', action: 'read' },
55
33
  { resource: 'posts', action: 'update', data: { authorId: 'u1' } },
56
34
  ]);
57
35
 
58
- // Decision trace candidates with index, score, priority, won (no logger fired)
59
- const trace = ward.trace(principal, 'posts', 'update', { authorId: 'u2' });
60
- trace.candidates.forEach((c) => console.log(`Rule[${c.index}]`, c.rule.effect, c.score, c.won));
36
+ const trace = ward.trace({
37
+ principal,
38
+ resource: 'posts',
39
+ action: 'update',
40
+ data: { authorId: 'u2' },
41
+ });
61
42
 
62
- // Principal-bound view — principal is snapshotted at bind time
63
43
  const bound = ward.forUser(principal);
64
- bound.allowedActions('posts', ['read', 'update', 'delete']);
65
- bound.explain('posts', 'update', { authorId: 'u2' });
66
-
67
- // Conflict detection — O(n²), lazy + cached
68
- const conflicts = ward.detectConflicts();
69
- if (conflicts.length > 0) console.warn('Policy conflicts:', conflicts);
44
+ bound.allowedActions({ resource: 'posts', knownActions: ['read', 'update', 'delete'] as const });
45
+ bound.explain({ resource: 'posts', action: 'update', data: { authorId: 'u2' } });
70
46
  ```
71
47
 
72
- ## Documentation
48
+ ## Middleware Guards
73
49
 
74
- - [Overview](https://vielzeug.dev/ward/)
75
- - [Usage Guide](https://vielzeug.dev/ward/usage)
76
- - [API Reference](https://vielzeug.dev/ward/api)
77
- - [Examples](https://vielzeug.dev/ward/examples)
50
+ ```ts
51
+ import { guardRequest, guardRequestWith } from '@vielzeug/ward';
52
+
53
+ const direct = guardRequest({
54
+ ward,
55
+ principal,
56
+ resource: 'posts',
57
+ action: 'read',
58
+ });
59
+
60
+ const withExtractor = await guardRequestWith({
61
+ ward,
62
+ req,
63
+ extractPrincipal: async (request) => request.user ?? null,
64
+ resource: 'posts',
65
+ action: 'read',
66
+ });
67
+ ```
78
68
 
79
- ## License
69
+ ## API Notes
80
70
 
81
- MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
71
+ 1. `explain()` and `trace()` take object inputs: `{ principal, resource, action, data? }`.
72
+ 2. `allowedActions()` takes `{ principal, resource, knownActions, data? }`.
73
+ 3. `rulesInScope()` takes `{ principal, resource, data? }`.
74
+ 4. `BoundWard` methods use object inputs without `principal`.
75
+ 5. `trace()` does not call the logger; `explain()` and `checkAll()` do.
package/dist/_match.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./constants.cjs"),t=require("./errors.cjs"),n=require("./_dev.cjs"),r=require("./resource.cjs");function i(e){if(typeof e!=`object`||!e)throw new t.WardConfigError(`Invalid principal: expected { id: string, roles: string[] }`);let n=e;if(typeof n.id!=`string`||!n.id.trim())throw new t.WardConfigError(`Invalid principal: id must be a non-empty string`);if(!Array.isArray(n.roles)||n.roles.some(e=>typeof e!=`string`||!e.trim()))throw new t.WardConfigError(`Invalid principal: roles must be an array of non-empty strings`)}function a(e){e!==null&&i(e)}function o(t,n){return n===null?t.includes(e.ANONYMOUS):t.every(e=>e===`anonymous`)?!1:t.includes(`*`)?!0:t.some(e=>n.roles.includes(e))}function s(e,i,a,s,c,l=!1){if(!o(e.roles,i)||!r.matchesPattern(e.rule.resource,a)||s!==void 0&&!r.matchesPattern(e.rule.action,s))return!1;if(l||!e.rule.when)return!0;if(i===null)return!1;try{let t=e.rule.when({data:c,principal:i});return t instanceof Promise&&n.error(`Rule[${e.index}] when() returned a Promise. Async predicates are not supported — the Promise is truthy and will always grant access. Use a sync predicate instead.`),t}catch(n){throw new t.WardPredicateError(e.index,n)}}function c(e,t){return t.priority>e.priority||t.priority===e.priority&&t.score>e.score||t.priority===e.priority&&t.score===e.score&&t.denyBonus>e.denyBonus}function l(e,t,n,r,i){let a;for(let o of e)s(o,t,n,r,i)&&(!a||c(a,o))&&(a=o);return a}function u(e){return e?e.rule.effect===`deny`?{allowed:!1,reason:`explicit-deny`,rule:e.rule}:{allowed:!0,rule:e.rule}:{allowed:!1,reason:`no-matching-rule`}}exports.assertUserPrincipal=i,exports.isOverriddenBy=c,exports.matchesRule=s,exports.pickWinner=l,exports.principalMatchesRoles=o,exports.toDecision=u,exports.validatePrincipal=a;
1
+ const e=require("./constants.cjs"),t=require("./errors.cjs"),n=require("./resource.cjs");function r(e){if(typeof e!=`object`||!e)throw new t.WardConfigError(`Invalid principal: expected { id: string, roles: string[] }`);let n=e;if(typeof n.id!=`string`||!n.id.trim())throw new t.WardConfigError(`Invalid principal: id must be a non-empty string`);if(!Array.isArray(n.roles)||n.roles.some(e=>typeof e!=`string`||!e.trim()))throw new t.WardConfigError(`Invalid principal: roles must be an array of non-empty strings`)}function i(e){e!==null&&r(e)}function a(t,n){return n===null?t.includes(e.ANONYMOUS):t.every(e=>e===`anonymous`)?!1:t.includes(`*`)?!0:t.some(e=>n.roles.includes(e))}function o(e,r,i,o,s,c=!1){if(!a(e.roles,r)||!n.matchesPattern(e.rule.resource,i)||o!==void 0&&!n.matchesPattern(e.rule.action,o))return!1;if(c||!e.rule.when)return!0;if(r===null)return!1;try{let t=e.rule.when({data:s,principal:r});if(t instanceof Promise)throw TypeError(`Rule[${e.index}] when() returned a Promise. Async predicates are not supported — use a synchronous predicate.`);return t}catch(n){throw new t.WardPredicateError(e.index,n)}}function s(e,t){return t.priority>e.priority||t.priority===e.priority&&t.score>e.score||t.priority===e.priority&&t.score===e.score&&t.denyBonus>e.denyBonus}function c(e,t,n,r,i){let a;for(let c of e)o(c,t,n,r,i)&&(!a||s(a,c))&&(a=c);return a}function l(e){return e?e.rule.effect===`deny`?{allowed:!1,reason:`explicit-deny`,rule:e.rule}:{allowed:!0,rule:e.rule}:{allowed:!1,reason:`no-matching-rule`}}exports.assertUserPrincipal=r,exports.isOverriddenBy=s,exports.matchesRule=o,exports.pickWinner=c,exports.principalMatchesRoles=a,exports.toDecision=l,exports.validatePrincipal=i;
2
2
  //# sourceMappingURL=_match.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"_match.cjs","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type { Principal, UserPrincipal, WardDecision, WardRule } from './types';\n\nimport { error } from './_dev';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\n\n// ---------------------------------------------------------------------------\n// Principal validation\n// ---------------------------------------------------------------------------\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const p = input as Record<string, unknown>;\n\n if (typeof p.id !== 'string' || !p.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(p.roles) || p.roles.some((r) => typeof r !== 'string' || !(r as string).trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function validatePrincipal(principal: Principal): void {\n if (principal !== null) {\n assertUserPrincipal(principal);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Role matching\n// ---------------------------------------------------------------------------\n\n/** Returns true if the principal's role set intersects with the rule's roles. */\nexport function principalMatchesRoles(roles: readonly string[], principal: Principal): boolean {\n if (principal === null) {\n return roles.includes(ANONYMOUS);\n }\n\n if (roles.every((r) => r === ANONYMOUS)) return false;\n\n if (roles.includes(WILDCARD)) return true;\n\n return roles.some((role) => principal.roles.includes(role));\n}\n\n// ---------------------------------------------------------------------------\n// Rule matching\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a rule applies to a given request.\n *\n * @param action - Pass `undefined` to skip the action check (used by `rulesInScope`).\n * @param data - Data payload; passed to `when` predicates.\n * @param skipPredicate - When true, omit the `when` evaluation even when data is present.\n */\nexport function matchesRule<TAction extends string, TData>(\n entry: CompiledEntry<TAction, TData>,\n principal: Principal,\n resource: string,\n action: TAction | undefined,\n data: TData | undefined,\n skipPredicate = false,\n): boolean {\n if (!principalMatchesRoles(entry.roles, principal)) return false;\n\n if (!matchesPattern(entry.rule.resource as string, resource)) return false;\n\n if (action !== undefined && !matchesPattern(entry.rule.action as string, action)) return false;\n\n if (skipPredicate || !entry.rule.when) return true;\n\n if (principal === null) return false;\n\n try {\n const result: unknown = entry.rule.when({ data, principal });\n\n if (result instanceof Promise) {\n error(\n `Rule[${entry.index}] when() returned a Promise. Async predicates are not supported — the Promise is truthy and will always grant access. Use a sync predicate instead.`,\n );\n }\n\n return result as boolean;\n } catch (err) {\n throw new WardPredicateError(entry.index, err);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Winner selection\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if `challenger` would displace `current` as the pickWinner result.\n */\nexport function isOverriddenBy<TAction extends string, TData>(\n current: CompiledEntry<TAction, TData>,\n challenger: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n challenger.priority > current.priority ||\n (challenger.priority === current.priority && challenger.score > current.score) ||\n (challenger.priority === current.priority &&\n challenger.score === current.score &&\n challenger.denyBonus > current.denyBonus)\n );\n}\n\nexport function pickWinner<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n): CompiledEntry<TAction, TData> | undefined {\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, action, data)) continue;\n\n if (!winner || isOverriddenBy(winner, entry)) {\n winner = entry;\n }\n }\n\n return winner;\n}\n\nexport function toDecision<TAction extends string, TData>(\n winner: CompiledEntry<TAction, TData> | undefined,\n): WardDecision<TAction, TData> {\n if (!winner) return { allowed: false, reason: 'no-matching-rule' };\n\n if (winner.rule.effect === 'deny') {\n return { allowed: false, reason: 'explicit-deny', rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n }\n\n return { allowed: true, rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n}\n"],"mappings":"iHAaA,SAAgB,EAAoB,EAAgD,CAClF,GAAI,OAAO,GAAU,UAAY,CAAC,EAChC,MAAM,IAAI,EAAA,gBAAgB,6DAA6D,EAGzF,IAAM,EAAI,EAEV,GAAI,OAAO,EAAE,IAAO,UAAY,CAAC,EAAE,GAAG,KAAK,EACzC,MAAM,IAAI,EAAA,gBAAgB,kDAAkD,EAG9E,GAAI,CAAC,MAAM,QAAQ,EAAE,KAAK,GAAK,EAAE,MAAM,KAAM,GAAM,OAAO,GAAM,UAAY,CAAE,EAAa,KAAK,CAAC,EAC/F,MAAM,IAAI,EAAA,gBAAgB,gEAAgE,CAE9F,CAEA,SAAgB,EAAkB,EAA4B,CACxD,IAAc,MAChB,EAAoB,CAAS,CAEjC,CAOA,SAAgB,EAAsB,EAA0B,EAA+B,CAS7F,OARI,IAAc,KACT,EAAM,SAAS,EAAA,SAAS,EAG7B,EAAM,MAAO,GAAM,IAAA,WAAe,EAAU,GAE5C,EAAM,SAAA,GAAiB,EAAU,GAE9B,EAAM,KAAM,GAAS,EAAU,MAAM,SAAS,CAAI,CAAC,CAC5D,CAaA,SAAgB,EACd,EACA,EACA,EACA,EACA,EACA,EAAgB,GACP,CAKT,GAJI,CAAC,EAAsB,EAAM,MAAO,CAAS,GAE7C,CAAC,EAAA,eAAe,EAAM,KAAK,SAAoB,CAAQ,GAEvD,IAAW,IAAA,IAAa,CAAC,EAAA,eAAe,EAAM,KAAK,OAAkB,CAAM,EAAG,MAAO,GAEzF,GAAI,GAAiB,CAAC,EAAM,KAAK,KAAM,MAAO,GAE9C,GAAI,IAAc,KAAM,MAAO,GAE/B,GAAI,CACF,IAAM,EAAkB,EAAM,KAAK,KAAK,CAAE,OAAM,WAAU,CAAC,EAQ3D,OANI,aAAkB,SACpB,EAAA,MACE,QAAQ,EAAM,MAAM,oJACtB,EAGK,CACT,OAAS,EAAK,CACZ,MAAM,IAAI,EAAA,mBAAmB,EAAM,MAAO,CAAG,CAC/C,CACF,CASA,SAAgB,EACd,EACA,EACS,CACT,OACE,EAAW,SAAW,EAAQ,UAC7B,EAAW,WAAa,EAAQ,UAAY,EAAW,MAAQ,EAAQ,OACvE,EAAW,WAAa,EAAQ,UAC/B,EAAW,QAAU,EAAQ,OAC7B,EAAW,UAAY,EAAQ,SAErC,CAEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAC2C,CAC3C,IAAI,EAEJ,IAAK,IAAM,KAAS,EACb,EAAY,EAAO,EAAW,EAAU,EAAQ,CAAI,IAErD,CAAC,GAAU,EAAe,EAAQ,CAAK,KACzC,EAAS,GAIb,OAAO,CACT,CAEA,SAAgB,EACd,EAC8B,CAO9B,OANK,EAED,EAAO,KAAK,SAAW,OAClB,CAAE,QAAS,GAAO,OAAQ,gBAAiB,KAAM,EAAO,IAA2C,EAGrG,CAAE,QAAS,GAAM,KAAM,EAAO,IAA2C,EAN5D,CAAE,QAAS,GAAO,OAAQ,kBAAmB,CAOnE"}
1
+ {"version":3,"file":"_match.cjs","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type { Principal, UserPrincipal, WardDecision, WardRule } from './types';\n\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\n\n// ---------------------------------------------------------------------------\n// Principal validation\n// ---------------------------------------------------------------------------\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const p = input as Record<string, unknown>;\n\n if (typeof p.id !== 'string' || !p.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(p.roles) || p.roles.some((r) => typeof r !== 'string' || !(r as string).trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function validatePrincipal(principal: Principal): void {\n if (principal !== null) {\n assertUserPrincipal(principal);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Role matching\n// ---------------------------------------------------------------------------\n\n/** Returns true if the principal's role set intersects with the rule's roles. */\nexport function principalMatchesRoles(roles: readonly string[], principal: Principal): boolean {\n if (principal === null) {\n return roles.includes(ANONYMOUS);\n }\n\n if (roles.every((r) => r === ANONYMOUS)) return false;\n\n if (roles.includes(WILDCARD)) return true;\n\n return roles.some((role) => principal.roles.includes(role));\n}\n\n// ---------------------------------------------------------------------------\n// Rule matching\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a rule applies to a given request.\n *\n * @param action - Pass `undefined` to skip the action check (used by `rulesInScope`).\n * @param data - Data payload; passed to `when` predicates.\n * @param skipPredicate - When true, omit the `when` evaluation even when data is present.\n */\nexport function matchesRule<TAction extends string, TData>(\n entry: CompiledEntry<TAction, TData>,\n principal: Principal,\n resource: string,\n action: TAction | undefined,\n data: TData | undefined,\n skipPredicate = false,\n): boolean {\n if (!principalMatchesRoles(entry.roles, principal)) return false;\n\n if (!matchesPattern(entry.rule.resource as string, resource)) return false;\n\n if (action !== undefined && !matchesPattern(entry.rule.action as string, action)) return false;\n\n if (skipPredicate || !entry.rule.when) return true;\n\n if (principal === null) return false;\n\n try {\n const result: unknown = entry.rule.when({ data, principal });\n\n if (result instanceof Promise) {\n throw new TypeError(\n `Rule[${entry.index}] when() returned a Promise. Async predicates are not supported — use a synchronous predicate.`,\n );\n }\n\n return result as boolean;\n } catch (err) {\n throw new WardPredicateError(entry.index, err);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Winner selection\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if `challenger` would displace `current` as the pickWinner result.\n */\nexport function isOverriddenBy<TAction extends string, TData>(\n current: CompiledEntry<TAction, TData>,\n challenger: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n challenger.priority > current.priority ||\n (challenger.priority === current.priority && challenger.score > current.score) ||\n (challenger.priority === current.priority &&\n challenger.score === current.score &&\n challenger.denyBonus > current.denyBonus)\n );\n}\n\nexport function pickWinner<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n): CompiledEntry<TAction, TData> | undefined {\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, action, data)) continue;\n\n if (!winner || isOverriddenBy(winner, entry)) {\n winner = entry;\n }\n }\n\n return winner;\n}\n\nexport function toDecision<TAction extends string, TData>(\n winner: CompiledEntry<TAction, TData> | undefined,\n): WardDecision<TAction, TData> {\n if (!winner) return { allowed: false, reason: 'no-matching-rule' };\n\n if (winner.rule.effect === 'deny') {\n return { allowed: false, reason: 'explicit-deny', rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n }\n\n return { allowed: true, rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n}\n"],"mappings":"yFAYA,SAAgB,EAAoB,EAAgD,CAClF,GAAI,OAAO,GAAU,UAAY,CAAC,EAChC,MAAM,IAAI,EAAA,gBAAgB,6DAA6D,EAGzF,IAAM,EAAI,EAEV,GAAI,OAAO,EAAE,IAAO,UAAY,CAAC,EAAE,GAAG,KAAK,EACzC,MAAM,IAAI,EAAA,gBAAgB,kDAAkD,EAG9E,GAAI,CAAC,MAAM,QAAQ,EAAE,KAAK,GAAK,EAAE,MAAM,KAAM,GAAM,OAAO,GAAM,UAAY,CAAE,EAAa,KAAK,CAAC,EAC/F,MAAM,IAAI,EAAA,gBAAgB,gEAAgE,CAE9F,CAEA,SAAgB,EAAkB,EAA4B,CACxD,IAAc,MAChB,EAAoB,CAAS,CAEjC,CAOA,SAAgB,EAAsB,EAA0B,EAA+B,CAS7F,OARI,IAAc,KACT,EAAM,SAAS,EAAA,SAAS,EAG7B,EAAM,MAAO,GAAM,IAAA,WAAe,EAAU,GAE5C,EAAM,SAAA,GAAiB,EAAU,GAE9B,EAAM,KAAM,GAAS,EAAU,MAAM,SAAS,CAAI,CAAC,CAC5D,CAaA,SAAgB,EACd,EACA,EACA,EACA,EACA,EACA,EAAgB,GACP,CAKT,GAJI,CAAC,EAAsB,EAAM,MAAO,CAAS,GAE7C,CAAC,EAAA,eAAe,EAAM,KAAK,SAAoB,CAAQ,GAEvD,IAAW,IAAA,IAAa,CAAC,EAAA,eAAe,EAAM,KAAK,OAAkB,CAAM,EAAG,MAAO,GAEzF,GAAI,GAAiB,CAAC,EAAM,KAAK,KAAM,MAAO,GAE9C,GAAI,IAAc,KAAM,MAAO,GAE/B,GAAI,CACF,IAAM,EAAkB,EAAM,KAAK,KAAK,CAAE,OAAM,WAAU,CAAC,EAE3D,GAAI,aAAkB,QACpB,MAAU,UACR,QAAQ,EAAM,MAAM,+FACtB,EAGF,OAAO,CACT,OAAS,EAAK,CACZ,MAAM,IAAI,EAAA,mBAAmB,EAAM,MAAO,CAAG,CAC/C,CACF,CASA,SAAgB,EACd,EACA,EACS,CACT,OACE,EAAW,SAAW,EAAQ,UAC7B,EAAW,WAAa,EAAQ,UAAY,EAAW,MAAQ,EAAQ,OACvE,EAAW,WAAa,EAAQ,UAC/B,EAAW,QAAU,EAAQ,OAC7B,EAAW,UAAY,EAAQ,SAErC,CAEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAC2C,CAC3C,IAAI,EAEJ,IAAK,IAAM,KAAS,EACb,EAAY,EAAO,EAAW,EAAU,EAAQ,CAAI,IAErD,CAAC,GAAU,EAAe,EAAQ,CAAK,KACzC,EAAS,GAIb,OAAO,CACT,CAEA,SAAgB,EACd,EAC8B,CAO9B,OANK,EAED,EAAO,KAAK,SAAW,OAClB,CAAE,QAAS,GAAO,OAAQ,gBAAiB,KAAM,EAAO,IAA2C,EAGrG,CAAE,QAAS,GAAM,KAAM,EAAO,IAA2C,EAN5D,CAAE,QAAS,GAAO,OAAQ,kBAAmB,CAOnE"}
@@ -1 +1 @@
1
- {"version":3,"file":"_match.d.ts","sourceRoot":"","sources":["../src/_match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAY,MAAM,SAAS,CAAC;AAWhF,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAclF;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAI5D;AAMD,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAU7F;AAMD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACvD,KAAK,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EACpC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,GAAG,SAAS,EAC3B,IAAI,EAAE,KAAK,GAAG,SAAS,EACvB,aAAa,UAAQ,GACpB,OAAO,CAwBT;AAMD;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EAC1D,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EACtC,UAAU,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,GACxC,OAAO,CAQT;AAED,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACtD,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EACxC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,KAAK,GAAG,SAAS,GACtB,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,SAAS,CAY3C;AAED,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACtD,MAAM,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,SAAS,GAChD,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAQ9B"}
1
+ {"version":3,"file":"_match.d.ts","sourceRoot":"","sources":["../src/_match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAY,MAAM,SAAS,CAAC;AAUhF,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAclF;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAI5D;AAMD,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAU7F;AAMD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACvD,KAAK,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EACpC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,GAAG,SAAS,EAC3B,IAAI,EAAE,KAAK,GAAG,SAAS,EACvB,aAAa,UAAQ,GACpB,OAAO,CAwBT;AAMD;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EAC1D,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EACtC,UAAU,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,GACxC,OAAO,CAQT;AAED,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACtD,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EACxC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,KAAK,GAAG,SAAS,GACtB,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,SAAS,CAY3C;AAED,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACtD,MAAM,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,SAAS,GAChD,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAQ9B"}
package/dist/_match.js CHANGED
@@ -1,43 +1,43 @@
1
1
  import { ANONYMOUS as e } from "./constants.js";
2
2
  import { WardConfigError as t, WardPredicateError as n } from "./errors.js";
3
- import { error as r } from "./_dev.js";
4
- import { matchesPattern as i } from "./resource.js";
3
+ import { matchesPattern as r } from "./resource.js";
5
4
  //#region src/_match.ts
6
- function a(e) {
5
+ function i(e) {
7
6
  if (typeof e != "object" || !e) throw new t("Invalid principal: expected { id: string, roles: string[] }");
8
7
  let n = e;
9
8
  if (typeof n.id != "string" || !n.id.trim()) throw new t("Invalid principal: id must be a non-empty string");
10
9
  if (!Array.isArray(n.roles) || n.roles.some((e) => typeof e != "string" || !e.trim())) throw new t("Invalid principal: roles must be an array of non-empty strings");
11
10
  }
12
- function o(e) {
13
- e !== null && a(e);
11
+ function a(e) {
12
+ e !== null && i(e);
14
13
  }
15
- function s(t, n) {
14
+ function o(t, n) {
16
15
  return n === null ? t.includes(e) : t.every((e) => e === "anonymous") ? !1 : t.includes("*") ? !0 : t.some((e) => n.roles.includes(e));
17
16
  }
18
- function c(e, t, a, o, c, l = !1) {
19
- if (!s(e.roles, t) || !i(e.rule.resource, a) || o !== void 0 && !i(e.rule.action, o)) return !1;
20
- if (l || !e.rule.when) return !0;
17
+ function s(e, t, i, a, s, c = !1) {
18
+ if (!o(e.roles, t) || !r(e.rule.resource, i) || a !== void 0 && !r(e.rule.action, a)) return !1;
19
+ if (c || !e.rule.when) return !0;
21
20
  if (t === null) return !1;
22
21
  try {
23
22
  let n = e.rule.when({
24
- data: c,
23
+ data: s,
25
24
  principal: t
26
25
  });
27
- return n instanceof Promise && r(`Rule[${e.index}] when() returned a Promise. Async predicates are not supported — the Promise is truthy and will always grant access. Use a sync predicate instead.`), n;
26
+ if (n instanceof Promise) throw TypeError(`Rule[${e.index}] when() returned a Promise. Async predicates are not supported — use a synchronous predicate.`);
27
+ return n;
28
28
  } catch (t) {
29
29
  throw new n(e.index, t);
30
30
  }
31
31
  }
32
- function l(e, t) {
32
+ function c(e, t) {
33
33
  return t.priority > e.priority || t.priority === e.priority && t.score > e.score || t.priority === e.priority && t.score === e.score && t.denyBonus > e.denyBonus;
34
34
  }
35
- function u(e, t, n, r, i) {
35
+ function l(e, t, n, r, i) {
36
36
  let a;
37
- for (let o of e) c(o, t, n, r, i) && (!a || l(a, o)) && (a = o);
37
+ for (let o of e) s(o, t, n, r, i) && (!a || c(a, o)) && (a = o);
38
38
  return a;
39
39
  }
40
- function d(e) {
40
+ function u(e) {
41
41
  return e ? e.rule.effect === "deny" ? {
42
42
  allowed: !1,
43
43
  reason: "explicit-deny",
@@ -51,6 +51,6 @@ function d(e) {
51
51
  };
52
52
  }
53
53
  //#endregion
54
- export { a as assertUserPrincipal, l as isOverriddenBy, c as matchesRule, u as pickWinner, s as principalMatchesRoles, d as toDecision, o as validatePrincipal };
54
+ export { i as assertUserPrincipal, c as isOverriddenBy, s as matchesRule, l as pickWinner, o as principalMatchesRoles, u as toDecision, a as validatePrincipal };
55
55
 
56
56
  //# sourceMappingURL=_match.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"_match.js","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type { Principal, UserPrincipal, WardDecision, WardRule } from './types';\n\nimport { error } from './_dev';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\n\n// ---------------------------------------------------------------------------\n// Principal validation\n// ---------------------------------------------------------------------------\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const p = input as Record<string, unknown>;\n\n if (typeof p.id !== 'string' || !p.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(p.roles) || p.roles.some((r) => typeof r !== 'string' || !(r as string).trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function validatePrincipal(principal: Principal): void {\n if (principal !== null) {\n assertUserPrincipal(principal);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Role matching\n// ---------------------------------------------------------------------------\n\n/** Returns true if the principal's role set intersects with the rule's roles. */\nexport function principalMatchesRoles(roles: readonly string[], principal: Principal): boolean {\n if (principal === null) {\n return roles.includes(ANONYMOUS);\n }\n\n if (roles.every((r) => r === ANONYMOUS)) return false;\n\n if (roles.includes(WILDCARD)) return true;\n\n return roles.some((role) => principal.roles.includes(role));\n}\n\n// ---------------------------------------------------------------------------\n// Rule matching\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a rule applies to a given request.\n *\n * @param action - Pass `undefined` to skip the action check (used by `rulesInScope`).\n * @param data - Data payload; passed to `when` predicates.\n * @param skipPredicate - When true, omit the `when` evaluation even when data is present.\n */\nexport function matchesRule<TAction extends string, TData>(\n entry: CompiledEntry<TAction, TData>,\n principal: Principal,\n resource: string,\n action: TAction | undefined,\n data: TData | undefined,\n skipPredicate = false,\n): boolean {\n if (!principalMatchesRoles(entry.roles, principal)) return false;\n\n if (!matchesPattern(entry.rule.resource as string, resource)) return false;\n\n if (action !== undefined && !matchesPattern(entry.rule.action as string, action)) return false;\n\n if (skipPredicate || !entry.rule.when) return true;\n\n if (principal === null) return false;\n\n try {\n const result: unknown = entry.rule.when({ data, principal });\n\n if (result instanceof Promise) {\n error(\n `Rule[${entry.index}] when() returned a Promise. Async predicates are not supported — the Promise is truthy and will always grant access. Use a sync predicate instead.`,\n );\n }\n\n return result as boolean;\n } catch (err) {\n throw new WardPredicateError(entry.index, err);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Winner selection\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if `challenger` would displace `current` as the pickWinner result.\n */\nexport function isOverriddenBy<TAction extends string, TData>(\n current: CompiledEntry<TAction, TData>,\n challenger: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n challenger.priority > current.priority ||\n (challenger.priority === current.priority && challenger.score > current.score) ||\n (challenger.priority === current.priority &&\n challenger.score === current.score &&\n challenger.denyBonus > current.denyBonus)\n );\n}\n\nexport function pickWinner<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n): CompiledEntry<TAction, TData> | undefined {\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, action, data)) continue;\n\n if (!winner || isOverriddenBy(winner, entry)) {\n winner = entry;\n }\n }\n\n return winner;\n}\n\nexport function toDecision<TAction extends string, TData>(\n winner: CompiledEntry<TAction, TData> | undefined,\n): WardDecision<TAction, TData> {\n if (!winner) return { allowed: false, reason: 'no-matching-rule' };\n\n if (winner.rule.effect === 'deny') {\n return { allowed: false, reason: 'explicit-deny', rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n }\n\n return { allowed: true, rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n}\n"],"mappings":";;;;;AAaA,SAAgB,EAAoB,GAAgD;CAClF,IAAI,OAAO,KAAU,YAAY,CAAC,GAChC,MAAM,IAAI,EAAgB,6DAA6D;CAGzF,IAAM,IAAI;CAEV,IAAI,OAAO,EAAE,MAAO,YAAY,CAAC,EAAE,GAAG,KAAK,GACzC,MAAM,IAAI,EAAgB,kDAAkD;CAG9E,IAAI,CAAC,MAAM,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,MAAM,MAAM,OAAO,KAAM,YAAY,CAAE,EAAa,KAAK,CAAC,GAC/F,MAAM,IAAI,EAAgB,gEAAgE;AAE9F;AAEA,SAAgB,EAAkB,GAA4B;CAC5D,AAAI,MAAc,QAChB,EAAoB,CAAS;AAEjC;AAOA,SAAgB,EAAsB,GAA0B,GAA+B;CAS7F,OARI,MAAc,OACT,EAAM,SAAS,CAAS,IAG7B,EAAM,OAAO,MAAM,MAAA,WAAe,IAAU,KAE5C,EAAM,SAAA,GAAiB,IAAU,KAE9B,EAAM,MAAM,MAAS,EAAU,MAAM,SAAS,CAAI,CAAC;AAC5D;AAaA,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,IAAgB,IACP;CAKT,IAJI,CAAC,EAAsB,EAAM,OAAO,CAAS,KAE7C,CAAC,EAAe,EAAM,KAAK,UAAoB,CAAQ,KAEvD,MAAW,KAAA,KAAa,CAAC,EAAe,EAAM,KAAK,QAAkB,CAAM,GAAG,OAAO;CAEzF,IAAI,KAAiB,CAAC,EAAM,KAAK,MAAM,OAAO;CAE9C,IAAI,MAAc,MAAM,OAAO;CAE/B,IAAI;EACF,IAAM,IAAkB,EAAM,KAAK,KAAK;GAAE;GAAM;EAAU,CAAC;EAQ3D,OANI,aAAkB,WACpB,EACE,QAAQ,EAAM,MAAM,oJACtB,GAGK;CACT,SAAS,GAAK;EACZ,MAAM,IAAI,EAAmB,EAAM,OAAO,CAAG;CAC/C;AACF;AASA,SAAgB,EACd,GACA,GACS;CACT,OACE,EAAW,WAAW,EAAQ,YAC7B,EAAW,aAAa,EAAQ,YAAY,EAAW,QAAQ,EAAQ,SACvE,EAAW,aAAa,EAAQ,YAC/B,EAAW,UAAU,EAAQ,SAC7B,EAAW,YAAY,EAAQ;AAErC;AAEA,SAAgB,EACd,GACA,GACA,GACA,GACA,GAC2C;CAC3C,IAAI;CAEJ,KAAK,IAAM,KAAS,GACb,EAAY,GAAO,GAAW,GAAU,GAAQ,CAAI,MAErD,CAAC,KAAU,EAAe,GAAQ,CAAK,OACzC,IAAS;CAIb,OAAO;AACT;AAEA,SAAgB,EACd,GAC8B;CAO9B,OANK,IAED,EAAO,KAAK,WAAW,SAClB;EAAE,SAAS;EAAO,QAAQ;EAAiB,MAAM,EAAO;CAA2C,IAGrG;EAAE,SAAS;EAAM,MAAM,EAAO;CAA2C,IAN5D;EAAE,SAAS;EAAO,QAAQ;CAAmB;AAOnE"}
1
+ {"version":3,"file":"_match.js","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type { Principal, UserPrincipal, WardDecision, WardRule } from './types';\n\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\n\n// ---------------------------------------------------------------------------\n// Principal validation\n// ---------------------------------------------------------------------------\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const p = input as Record<string, unknown>;\n\n if (typeof p.id !== 'string' || !p.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(p.roles) || p.roles.some((r) => typeof r !== 'string' || !(r as string).trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function validatePrincipal(principal: Principal): void {\n if (principal !== null) {\n assertUserPrincipal(principal);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Role matching\n// ---------------------------------------------------------------------------\n\n/** Returns true if the principal's role set intersects with the rule's roles. */\nexport function principalMatchesRoles(roles: readonly string[], principal: Principal): boolean {\n if (principal === null) {\n return roles.includes(ANONYMOUS);\n }\n\n if (roles.every((r) => r === ANONYMOUS)) return false;\n\n if (roles.includes(WILDCARD)) return true;\n\n return roles.some((role) => principal.roles.includes(role));\n}\n\n// ---------------------------------------------------------------------------\n// Rule matching\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a rule applies to a given request.\n *\n * @param action - Pass `undefined` to skip the action check (used by `rulesInScope`).\n * @param data - Data payload; passed to `when` predicates.\n * @param skipPredicate - When true, omit the `when` evaluation even when data is present.\n */\nexport function matchesRule<TAction extends string, TData>(\n entry: CompiledEntry<TAction, TData>,\n principal: Principal,\n resource: string,\n action: TAction | undefined,\n data: TData | undefined,\n skipPredicate = false,\n): boolean {\n if (!principalMatchesRoles(entry.roles, principal)) return false;\n\n if (!matchesPattern(entry.rule.resource as string, resource)) return false;\n\n if (action !== undefined && !matchesPattern(entry.rule.action as string, action)) return false;\n\n if (skipPredicate || !entry.rule.when) return true;\n\n if (principal === null) return false;\n\n try {\n const result: unknown = entry.rule.when({ data, principal });\n\n if (result instanceof Promise) {\n throw new TypeError(\n `Rule[${entry.index}] when() returned a Promise. Async predicates are not supported — use a synchronous predicate.`,\n );\n }\n\n return result as boolean;\n } catch (err) {\n throw new WardPredicateError(entry.index, err);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Winner selection\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if `challenger` would displace `current` as the pickWinner result.\n */\nexport function isOverriddenBy<TAction extends string, TData>(\n current: CompiledEntry<TAction, TData>,\n challenger: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n challenger.priority > current.priority ||\n (challenger.priority === current.priority && challenger.score > current.score) ||\n (challenger.priority === current.priority &&\n challenger.score === current.score &&\n challenger.denyBonus > current.denyBonus)\n );\n}\n\nexport function pickWinner<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n): CompiledEntry<TAction, TData> | undefined {\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, action, data)) continue;\n\n if (!winner || isOverriddenBy(winner, entry)) {\n winner = entry;\n }\n }\n\n return winner;\n}\n\nexport function toDecision<TAction extends string, TData>(\n winner: CompiledEntry<TAction, TData> | undefined,\n): WardDecision<TAction, TData> {\n if (!winner) return { allowed: false, reason: 'no-matching-rule' };\n\n if (winner.rule.effect === 'deny') {\n return { allowed: false, reason: 'explicit-deny', rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n }\n\n return { allowed: true, rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n}\n"],"mappings":";;;;AAYA,SAAgB,EAAoB,GAAgD;CAClF,IAAI,OAAO,KAAU,YAAY,CAAC,GAChC,MAAM,IAAI,EAAgB,6DAA6D;CAGzF,IAAM,IAAI;CAEV,IAAI,OAAO,EAAE,MAAO,YAAY,CAAC,EAAE,GAAG,KAAK,GACzC,MAAM,IAAI,EAAgB,kDAAkD;CAG9E,IAAI,CAAC,MAAM,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,MAAM,MAAM,OAAO,KAAM,YAAY,CAAE,EAAa,KAAK,CAAC,GAC/F,MAAM,IAAI,EAAgB,gEAAgE;AAE9F;AAEA,SAAgB,EAAkB,GAA4B;CAC5D,AAAI,MAAc,QAChB,EAAoB,CAAS;AAEjC;AAOA,SAAgB,EAAsB,GAA0B,GAA+B;CAS7F,OARI,MAAc,OACT,EAAM,SAAS,CAAS,IAG7B,EAAM,OAAO,MAAM,MAAA,WAAe,IAAU,KAE5C,EAAM,SAAA,GAAiB,IAAU,KAE9B,EAAM,MAAM,MAAS,EAAU,MAAM,SAAS,CAAI,CAAC;AAC5D;AAaA,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,IAAgB,IACP;CAKT,IAJI,CAAC,EAAsB,EAAM,OAAO,CAAS,KAE7C,CAAC,EAAe,EAAM,KAAK,UAAoB,CAAQ,KAEvD,MAAW,KAAA,KAAa,CAAC,EAAe,EAAM,KAAK,QAAkB,CAAM,GAAG,OAAO;CAEzF,IAAI,KAAiB,CAAC,EAAM,KAAK,MAAM,OAAO;CAE9C,IAAI,MAAc,MAAM,OAAO;CAE/B,IAAI;EACF,IAAM,IAAkB,EAAM,KAAK,KAAK;GAAE;GAAM;EAAU,CAAC;EAE3D,IAAI,aAAkB,SACpB,MAAU,UACR,QAAQ,EAAM,MAAM,+FACtB;EAGF,OAAO;CACT,SAAS,GAAK;EACZ,MAAM,IAAI,EAAmB,EAAM,OAAO,CAAG;CAC/C;AACF;AASA,SAAgB,EACd,GACA,GACS;CACT,OACE,EAAW,WAAW,EAAQ,YAC7B,EAAW,aAAa,EAAQ,YAAY,EAAW,QAAQ,EAAQ,SACvE,EAAW,aAAa,EAAQ,YAC/B,EAAW,UAAU,EAAQ,SAC7B,EAAW,YAAY,EAAQ;AAErC;AAEA,SAAgB,EACd,GACA,GACA,GACA,GACA,GAC2C;CAC3C,IAAI;CAEJ,KAAK,IAAM,KAAS,GACb,EAAY,GAAO,GAAW,GAAU,GAAQ,CAAI,MAErD,CAAC,KAAU,EAAe,GAAQ,CAAK,OACzC,IAAS;CAIb,OAAO;AACT;AAEA,SAAgB,EACd,GAC8B;CAO9B,OANK,IAED,EAAO,KAAK,WAAW,SAClB;EAAE,SAAS;EAAO,QAAQ;EAAiB,MAAM,EAAO;CAA2C,IAGrG;EAAE,SAAS;EAAM,MAAM,EAAO;CAA2C,IAN5D;EAAE,SAAS;EAAO,QAAQ;CAAmB;AAOnE"}
@@ -1 +1 @@
1
- {"version":3,"file":"devtools.cjs","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/ward — debug utilities for authorization decision visualisation.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n * ```\n */\n\nimport type { Ward, WardLoggerContext, WardOptions, WardRule } from './types';\n\nimport { createWard } from './factory';\n\n/**\n * Creates a {@link Ward} with authorization decision logging pre-wired to `console.debug`.\n *\n * Equivalent to `createWard(rules, { ...options, logger: ctx => console.debug(...) })` but\n * imported from a dedicated sub-path so `console.debug` references are tree-shaken from\n * production bundles when this sub-path is not imported.\n *\n * Logs every authorization decision made by any decision method (`checkAll`, `explain`)\n * with `[ward:decision]` prefixes showing the outcome,\n * principal, resource, action, and — when a rule matched — its effect.\n *\n * @example\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n *\n * const permit = debugWard(rules);\n * permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'read');\n * // [ward:decision] allow (allow) viewer posts read\n *\n * permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'delete');\n * // [ward:decision] no-matching-rule viewer posts delete\n *\n * // Note: `trace()` does NOT fire the logger — it is a side-channel-free inspection tool.\n * ```\n */\nexport function debugWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[],\n options?: Omit<WardOptions<TAction, TData>, 'logger'>,\n): Ward<TAction, TData> {\n const logger = (ctx: WardLoggerContext<TAction, TData>): void => {\n const principal = ctx.principal\n ? ctx.principal.roles.length > 0\n ? ctx.principal.roles.join(', ')\n : ctx.principal.id\n : 'anonymous';\n const outcome = ctx.allowed ? 'allow' : ctx.reason;\n const decision = outcome.padEnd(16);\n const effect = 'rule' in ctx ? `(${ctx.rule.effect})`.padEnd(8) : ' ';\n\n console.debug(`[ward:decision] ${decision} ${effect} ${principal} ${ctx.resource} ${ctx.action}`);\n };\n\n return createWard<TAction, TData>(rules, { ...options, logger });\n}\n"],"mappings":"oGAsCA,SAAgB,EACd,EACA,EACsB,CACtB,IAAM,EAAU,GAAiD,CAC/D,IAAM,EAAY,EAAI,UAClB,EAAI,UAAU,MAAM,OAAS,EAC3B,EAAI,UAAU,MAAM,KAAK,IAAI,EAC7B,EAAI,UAAU,GAChB,YAEE,GADU,EAAI,QAAU,QAAU,EAAI,OAAA,CACnB,OAAO,EAAE,EAC5B,EAAS,SAAU,EAAM,IAAI,EAAI,KAAK,OAAO,GAAG,OAAO,CAAC,EAAI,WAElE,QAAQ,MAAM,mBAAmB,EAAS,IAAI,EAAO,IAAI,EAAU,IAAI,EAAI,SAAS,IAAI,EAAI,QAAQ,CACtG,EAEA,OAAO,EAAA,WAA2B,EAAO,CAAE,GAAG,EAAS,QAAO,CAAC,CACjE"}
1
+ {"version":3,"file":"devtools.cjs","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/ward — debug utilities for authorization decision visualisation.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n * ```\n */\n\nimport type { Ward, WardLoggerContext, WardOptions, WardRule } from './types';\n\nimport { createWard } from './factory';\n\n/**\n * Creates a {@link Ward} with authorization decision logging pre-wired to `console.debug`.\n *\n * Equivalent to `createWard(rules, { ...options, logger: ctx => console.debug(...) })` but\n * imported from a dedicated sub-path so `console.debug` references are tree-shaken from\n * production bundles when this sub-path is not imported.\n *\n * Logs every authorization decision made by any decision method (`checkAll`, `explain`)\n * with `[ward:decision]` prefixes showing the outcome,\n * principal, resource, action, and — when a rule matched — its effect.\n *\n * @example\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n *\n * const permit = debugWard(rules);\n * permit.explain({ action: 'read', principal: { id: 'u1', roles: ['viewer'] }, resource: 'posts' });\n * // [ward:decision] allow (allow) viewer posts read\n *\n * permit.explain({ action: 'delete', principal: { id: 'u1', roles: ['viewer'] }, resource: 'posts' });\n * // [ward:decision] no-matching-rule viewer posts delete\n *\n * // Note: `trace()` does NOT fire the logger — it is a side-channel-free inspection tool.\n * ```\n */\nexport function debugWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[],\n options?: Omit<WardOptions<TAction, TData>, 'logger'>,\n): Ward<TAction, TData> {\n const logger = (ctx: WardLoggerContext<TAction, TData>): void => {\n const principal = ctx.principal\n ? ctx.principal.roles.length > 0\n ? ctx.principal.roles.join(', ')\n : ctx.principal.id\n : 'anonymous';\n const outcome = ctx.allowed ? 'allow' : ctx.reason;\n const decision = outcome.padEnd(16);\n const effect = 'rule' in ctx ? `(${ctx.rule.effect})`.padEnd(8) : ' ';\n\n console.debug(`[ward:decision] ${decision} ${effect} ${principal} ${ctx.resource} ${ctx.action}`);\n };\n\n return createWard<TAction, TData>(rules, { ...options, logger });\n}\n"],"mappings":"oGAsCA,SAAgB,EACd,EACA,EACsB,CACtB,IAAM,EAAU,GAAiD,CAC/D,IAAM,EAAY,EAAI,UAClB,EAAI,UAAU,MAAM,OAAS,EAC3B,EAAI,UAAU,MAAM,KAAK,IAAI,EAC7B,EAAI,UAAU,GAChB,YAEE,GADU,EAAI,QAAU,QAAU,EAAI,OAAA,CACnB,OAAO,EAAE,EAC5B,EAAS,SAAU,EAAM,IAAI,EAAI,KAAK,OAAO,GAAG,OAAO,CAAC,EAAI,WAElE,QAAQ,MAAM,mBAAmB,EAAS,IAAI,EAAO,IAAI,EAAU,IAAI,EAAI,SAAS,IAAI,EAAI,QAAQ,CACtG,EAEA,OAAO,EAAA,WAA2B,EAAO,CAAE,GAAG,EAAS,QAAO,CAAC,CACjE"}
@@ -23,10 +23,10 @@ import type { Ward, WardOptions, WardRule } from './types';
23
23
  * import { debugWard } from '@vielzeug/ward/devtools';
24
24
  *
25
25
  * const permit = debugWard(rules);
26
- * permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'read');
26
+ * permit.explain({ action: 'read', principal: { id: 'u1', roles: ['viewer'] }, resource: 'posts' });
27
27
  * // [ward:decision] allow (allow) viewer posts read
28
28
  *
29
- * permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'delete');
29
+ * permit.explain({ action: 'delete', principal: { id: 'u1', roles: ['viewer'] }, resource: 'posts' });
30
30
  * // [ward:decision] no-matching-rule viewer posts delete
31
31
  *
32
32
  * // Note: `trace()` does NOT fire the logger — it is a side-channel-free inspection tool.
@@ -1 +1 @@
1
- {"version":3,"file":"devtools.js","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/ward — debug utilities for authorization decision visualisation.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n * ```\n */\n\nimport type { Ward, WardLoggerContext, WardOptions, WardRule } from './types';\n\nimport { createWard } from './factory';\n\n/**\n * Creates a {@link Ward} with authorization decision logging pre-wired to `console.debug`.\n *\n * Equivalent to `createWard(rules, { ...options, logger: ctx => console.debug(...) })` but\n * imported from a dedicated sub-path so `console.debug` references are tree-shaken from\n * production bundles when this sub-path is not imported.\n *\n * Logs every authorization decision made by any decision method (`checkAll`, `explain`)\n * with `[ward:decision]` prefixes showing the outcome,\n * principal, resource, action, and — when a rule matched — its effect.\n *\n * @example\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n *\n * const permit = debugWard(rules);\n * permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'read');\n * // [ward:decision] allow (allow) viewer posts read\n *\n * permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'delete');\n * // [ward:decision] no-matching-rule viewer posts delete\n *\n * // Note: `trace()` does NOT fire the logger — it is a side-channel-free inspection tool.\n * ```\n */\nexport function debugWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[],\n options?: Omit<WardOptions<TAction, TData>, 'logger'>,\n): Ward<TAction, TData> {\n const logger = (ctx: WardLoggerContext<TAction, TData>): void => {\n const principal = ctx.principal\n ? ctx.principal.roles.length > 0\n ? ctx.principal.roles.join(', ')\n : ctx.principal.id\n : 'anonymous';\n const outcome = ctx.allowed ? 'allow' : ctx.reason;\n const decision = outcome.padEnd(16);\n const effect = 'rule' in ctx ? `(${ctx.rule.effect})`.padEnd(8) : ' ';\n\n console.debug(`[ward:decision] ${decision} ${effect} ${principal} ${ctx.resource} ${ctx.action}`);\n };\n\n return createWard<TAction, TData>(rules, { ...options, logger });\n}\n"],"mappings":";;AAsCA,SAAgB,EACd,GACA,GACsB;CACtB,IAAM,KAAU,MAAiD;EAC/D,IAAM,IAAY,EAAI,YAClB,EAAI,UAAU,MAAM,SAAS,IAC3B,EAAI,UAAU,MAAM,KAAK,IAAI,IAC7B,EAAI,UAAU,KAChB,aAEE,KADU,EAAI,UAAU,UAAU,EAAI,OAAA,CACnB,OAAO,EAAE,GAC5B,IAAS,UAAU,IAAM,IAAI,EAAI,KAAK,OAAO,GAAG,OAAO,CAAC,IAAI;EAElE,QAAQ,MAAM,mBAAmB,EAAS,IAAI,EAAO,IAAI,EAAU,IAAI,EAAI,SAAS,IAAI,EAAI,QAAQ;CACtG;CAEA,OAAO,EAA2B,GAAO;EAAE,GAAG;EAAS;CAAO,CAAC;AACjE"}
1
+ {"version":3,"file":"devtools.js","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/ward — debug utilities for authorization decision visualisation.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n * ```\n */\n\nimport type { Ward, WardLoggerContext, WardOptions, WardRule } from './types';\n\nimport { createWard } from './factory';\n\n/**\n * Creates a {@link Ward} with authorization decision logging pre-wired to `console.debug`.\n *\n * Equivalent to `createWard(rules, { ...options, logger: ctx => console.debug(...) })` but\n * imported from a dedicated sub-path so `console.debug` references are tree-shaken from\n * production bundles when this sub-path is not imported.\n *\n * Logs every authorization decision made by any decision method (`checkAll`, `explain`)\n * with `[ward:decision]` prefixes showing the outcome,\n * principal, resource, action, and — when a rule matched — its effect.\n *\n * @example\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n *\n * const permit = debugWard(rules);\n * permit.explain({ action: 'read', principal: { id: 'u1', roles: ['viewer'] }, resource: 'posts' });\n * // [ward:decision] allow (allow) viewer posts read\n *\n * permit.explain({ action: 'delete', principal: { id: 'u1', roles: ['viewer'] }, resource: 'posts' });\n * // [ward:decision] no-matching-rule viewer posts delete\n *\n * // Note: `trace()` does NOT fire the logger — it is a side-channel-free inspection tool.\n * ```\n */\nexport function debugWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[],\n options?: Omit<WardOptions<TAction, TData>, 'logger'>,\n): Ward<TAction, TData> {\n const logger = (ctx: WardLoggerContext<TAction, TData>): void => {\n const principal = ctx.principal\n ? ctx.principal.roles.length > 0\n ? ctx.principal.roles.join(', ')\n : ctx.principal.id\n : 'anonymous';\n const outcome = ctx.allowed ? 'allow' : ctx.reason;\n const decision = outcome.padEnd(16);\n const effect = 'rule' in ctx ? `(${ctx.rule.effect})`.padEnd(8) : ' ';\n\n console.debug(`[ward:decision] ${decision} ${effect} ${principal} ${ctx.resource} ${ctx.action}`);\n };\n\n return createWard<TAction, TData>(rules, { ...options, logger });\n}\n"],"mappings":";;AAsCA,SAAgB,EACd,GACA,GACsB;CACtB,IAAM,KAAU,MAAiD;EAC/D,IAAM,IAAY,EAAI,YAClB,EAAI,UAAU,MAAM,SAAS,IAC3B,EAAI,UAAU,MAAM,KAAK,IAAI,IAC7B,EAAI,UAAU,KAChB,aAEE,KADU,EAAI,UAAU,UAAU,EAAI,OAAA,CACnB,OAAO,EAAE,GAC5B,IAAS,UAAU,IAAM,IAAI,EAAI,KAAK,OAAO,GAAG,OAAO,CAAC,IAAI;EAElE,QAAQ,MAAM,mBAAmB,EAAS,IAAI,EAAO,IAAI,EAAU,IAAI,EAAI,SAAS,IAAI,EAAI,QAAQ;CACtG;CAEA,OAAO,EAA2B,GAAO;EAAE,GAAG;EAAS;CAAO,CAAC;AACjE"}
package/dist/factory.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs"),t=require("./_compile.cjs"),n=require("./_match.cjs"),r=require("./_conflict.cjs");function i(e,t,r,i,a){let o=new Set,s=[];for(let c of i)o.has(c)||(o.add(c),n.pickWinner(e,t,r,c,a)?.rule.effect===`allow`&&s.push(c));return s}function a(e,t,r,i){let a=i===void 0,o=[];for(let s of e)n.matchesRule(s,t,r,void 0,i,a)&&o.push(s.rule);return o}function o(o=[],s={}){let{logger:c,maxConflicts:l=1/0}=s,u=o.map((e,n)=>t.compileEntry(e,n));function d(e,t,n,r,i){c&&c({...i,action:n,data:r,principal:e,resource:t})}function f(e,t,r,i){let a=n.toDecision(n.pickWinner(u,e,t,r,i));return d(e,t,r,i,a),a}function p(e,t,r,i){return n.validatePrincipal(e),f(e,t,r,i)}function m(e,t){return t.map(t=>({...f(e,t.resource,t.action,t.data),action:t.action,resource:t.resource}))}function h(e,t){return t.length===0?[]:(n.validatePrincipal(e),m(e,t))}function g(e,t,r,a){return n.validatePrincipal(e),i(u,e,t,r,a)}function _(e,t,r){return n.validatePrincipal(e),a(u,e,t,r)}function v(e,t,r,i){n.validatePrincipal(e);let a=[];for(let o of u)n.matchesRule(o,e,t,r,i)&&a.push(o);let o;for(let e of a)(!o||n.isOverriddenBy(o,e))&&(o=e);let s=n.toDecision(o);return{candidates:a.map(e=>({index:e.index,priority:e.priority,rule:e.rule,score:e.score,won:e===o})),decision:s}}function y(e){n.assertUserPrincipal(e);let t={attributes:e.attributes?structuredClone(e.attributes):void 0,id:e.id,roles:[...e.roles]};return{allowedActions:(e,n,r)=>i(u,t,e,n,r),checkAll:e=>e.length===0?[]:m(t,e),explain:(e,n,r)=>f(t,e,n,r),rulesInScope:(e,n)=>a(u,t,e,n),trace:(e,n,r)=>v(t,e,n,r)}}let b;function x(){return b??=Object.freeze(r.computeConflicts(u,l))}if(s.strict||s.onConflict){let t=x();if(t.length>0&&(s.onConflict&&t.forEach(s.onConflict),s.strict)){let n=t.map(e=>e.kind===`duplicate`?`Rule[${e.indexB}] ${e.kind} of Rule[${e.indexA}]`:`Rule[${e.shadowedIndex}] ${e.kind} by Rule[${e.shadowingIndex}]`).join(`; `);throw new e.WardConfigError(`${t.length} rule conflict(s) detected: ${n}`)}}return{allowedActions:g,checkAll:h,detectConflicts:x,explain:p,forUser:y,rulesInScope:_,trace:v}}exports.createWard=o;
1
+ const e=require("./errors.cjs"),t=require("./_compile.cjs"),n=require("./_match.cjs"),r=require("./_conflict.cjs");function i(e,t,r,i,a){let o=new Set,s=[];for(let c of i)o.has(c)||(o.add(c),n.pickWinner(e,t,r,c,a)?.rule.effect===`allow`&&s.push(c));return s}function a(e,t,r,i){let a=i===void 0,o=[];for(let s of e)n.matchesRule(s,t,r,void 0,i,a)&&o.push(s.rule);return o}function o(o=[],s={}){let{logger:c,maxConflicts:l=1/0}=s,u=o.map((e,n)=>t.compileEntry(e,n));function d(e,t,n,r,i){c&&c({...i,action:n,data:r,principal:e,resource:t})}function f(e,t,r,i){let a=n.toDecision(n.pickWinner(u,e,t,r,i));return d(e,t,r,i,a),a}function p(e){let{action:t,data:r,principal:i,resource:a}=e;return n.validatePrincipal(i),f(i,a,t,r)}function m(e,t){return t.map(t=>({...f(e,t.resource,t.action,t.data),action:t.action,resource:t.resource}))}function h(e,t){return t.length===0?[]:(n.validatePrincipal(e),m(e,t))}function g(e){let{data:t,knownActions:r,principal:a,resource:o}=e;return n.validatePrincipal(a),i(u,a,o,r,t)}function _(e){let{data:t,principal:r,resource:i}=e;return n.validatePrincipal(r),a(u,r,i,t)}function v(e){let{action:t,data:r,principal:i,resource:a}=e;n.validatePrincipal(i);let o=[];for(let e of u)n.matchesRule(e,i,a,t,r)&&o.push(e);let s;for(let e of o)(!s||n.isOverriddenBy(s,e))&&(s=e);let c=n.toDecision(s);return{candidates:o.map(e=>({index:e.index,priority:e.priority,rule:e.rule,score:e.score,won:e===s})),decision:c}}function y(e){n.assertUserPrincipal(e);let t={attributes:e.attributes?structuredClone(e.attributes):void 0,id:e.id,roles:[...e.roles]};return{allowedActions:e=>i(u,t,e.resource,e.knownActions,e.data),checkAll:e=>e.length===0?[]:m(t,e),explain:e=>f(t,e.resource,e.action,e.data),rulesInScope:e=>a(u,t,e.resource,e.data),trace:e=>v({action:e.action,data:e.data,principal:t,resource:e.resource})}}let b;function x(){return b??=Object.freeze(r.computeConflicts(u,l))}if(s.strict||s.onConflict){let t=x();if(t.length>0&&(s.onConflict&&t.forEach(s.onConflict),s.strict)){let n=t.map(e=>e.kind===`duplicate`?`Rule[${e.indexB}] ${e.kind} of Rule[${e.indexA}]`:`Rule[${e.shadowedIndex}] ${e.kind} by Rule[${e.shadowingIndex}]`).join(`; `);throw new e.WardConfigError(`${t.length} rule conflict(s) detected: ${n}`)}}return{allowedActions:g,checkAll:h,detectConflicts:x,explain:p,forUser:y,rulesInScope:_,trace:v}}exports.createWard=o;
2
2
  //# sourceMappingURL=factory.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"factory.cjs","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type {\n BoundWard,\n Principal,\n UserPrincipal,\n Ward,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionResult,\n WardOptions,\n WardRule,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\n\n// ---------------------------------------------------------------------------\n// Shared loop cores (validation-free; used by both public API and forUser)\n// ---------------------------------------------------------------------------\n\nfunction coreAllowedActions<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data: TData | undefined,\n): TAction[] {\n const seen = new Set<TAction>();\n const result: TAction[] = [];\n\n for (const action of knownActions) {\n if (seen.has(action)) continue;\n\n seen.add(action);\n\n const winner = pickWinner(entries, principal, resource, action, data);\n\n if (winner?.rule.effect === 'allow') result.push(action);\n }\n\n return result;\n}\n\nfunction coreRulesInScope<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n data: TData | undefined,\n): CompiledEntry<TAction, TData>['rule'][] {\n const skipPredicate = data === undefined;\n const result: CompiledEntry<TAction, TData>['rule'][] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n const { logger, maxConflicts = Infinity } = options;\n const entries = rules.map((rule, i) => compileEntry(rule, i));\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function fireLogger(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n decision: WardDecision<TAction, TData>,\n ): void {\n if (!logger) return;\n\n logger({ ...decision, action, data, principal, resource } as Parameters<typeof logger>[0]);\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n fireLogger(principal, resource, action, data, decision);\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n function explain(\n principal: Principal,\n resource: string,\n action: TAction,\n data?: TData,\n ): WardDecision<TAction, TData> {\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data?: TData,\n ): TAction[] {\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(\n principal: Principal,\n resource: string,\n data?: TData,\n ): ReadonlyArray<Readonly<WardRule<TAction, TData>>> {\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(principal: Principal, resource: string, action: TAction, data?: TData): WardTrace<TAction, TData> {\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (resource, knownActions, data?) =>\n coreAllowedActions(entries, snap, resource, knownActions, data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (resource, action, data?) => evaluateAndLog(snap, resource, action, data),\n rulesInScope: (resource, data?) => coreRulesInScope(entries, snap, resource, data),\n trace: (resource, action, data?) => trace(snap, resource, action, data),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, trace };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"factory.cjs","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type {\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n BoundWard,\n Principal,\n UserPrincipal,\n WardAllowedActionsInput,\n Ward,\n WardCheck,\n WardConflict,\n WardDecisionInput,\n WardDecision,\n WardDecisionResult,\n WardOptions,\n WardRulesInScopeInput,\n WardRule,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\n\n// ---------------------------------------------------------------------------\n// Shared loop cores (validation-free; used by both public API and forUser)\n// ---------------------------------------------------------------------------\n\nfunction coreAllowedActions<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data: TData | undefined,\n): TAction[] {\n const seen = new Set<TAction>();\n const result: TAction[] = [];\n\n for (const action of knownActions) {\n if (seen.has(action)) continue;\n\n seen.add(action);\n\n const winner = pickWinner(entries, principal, resource, action, data);\n\n if (winner?.rule.effect === 'allow') result.push(action);\n }\n\n return result;\n}\n\nfunction coreRulesInScope<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n data: TData | undefined,\n): CompiledEntry<TAction, TData>['rule'][] {\n const skipPredicate = data === undefined;\n const result: CompiledEntry<TAction, TData>['rule'][] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n const { logger, maxConflicts = Infinity } = options;\n const entries = rules.map((rule, i) => compileEntry(rule, i));\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function fireLogger(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n decision: WardDecision<TAction, TData>,\n ): void {\n if (!logger) return;\n\n logger({ ...decision, action, data, principal, resource } as Parameters<typeof logger>[0]);\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n fireLogger(principal, resource, action, data, decision);\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n // Request objects avoid call-site ambiguity between resource/action/data and\n // make later API growth additive instead of positional-breaking.\n\n function explain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[] {\n const { data, knownActions, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(input: WardRulesInScopeInput<TData>): ReadonlyArray<Readonly<WardRule<TAction, TData>>> {\n const { data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (input: BoundWardAllowedActionsInput<TAction, TData>) =>\n coreAllowedActions(entries, snap, input.resource, input.knownActions, input.data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (input: BoundWardDecisionInput<TAction, TData>) =>\n evaluateAndLog(snap, input.resource, input.action, input.data),\n rulesInScope: (input: BoundWardRulesInScopeInput<TData>) =>\n coreRulesInScope(entries, snap, input.resource, input.data),\n trace: (input: BoundWardDecisionInput<TAction, TData>) =>\n trace({ action: input.action, data: input.data, principal: snap, resource: input.resource }),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, trace };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,IAAI,EAKJ,WAAW,EACX,QAAQ,EAGT,MAAM,SAAS,CAAC;AAwDjB;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACzE,KAAK,GAAE,SAAS,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAO,EAC/C,OAAO,GAAE,WAAW,CAAC,OAAO,EAAE,KAAK,CAAM,GACxC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CA4KtB"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAQV,IAAI,EAMJ,WAAW,EAEX,QAAQ,EAGT,MAAM,SAAS,CAAC;AAwDjB;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACzE,KAAK,GAAE,SAAS,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAO,EAC/C,OAAO,GAAE,WAAW,CAAC,OAAO,EAAE,KAAK,CAAM,GACxC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CA2KtB"}
package/dist/factory.js CHANGED
@@ -28,8 +28,9 @@ function d(d = [], f = {}) {
28
28
  let i = o(a(h, e, t, n, r));
29
29
  return g(e, t, n, r, i), i;
30
30
  }
31
- function v(e, t, n, r) {
32
- return s(e), _(e, t, n, r);
31
+ function v(e) {
32
+ let { action: t, data: n, principal: r, resource: i } = e;
33
+ return s(r), _(r, i, t, n);
33
34
  }
34
35
  function y(e, t) {
35
36
  return t.map((t) => ({
@@ -41,28 +42,31 @@ function d(d = [], f = {}) {
41
42
  function b(e, t) {
42
43
  return t.length === 0 ? [] : (s(e), y(e, t));
43
44
  }
44
- function x(e, t, n, r) {
45
- return s(e), l(h, e, t, n, r);
45
+ function x(e) {
46
+ let { data: t, knownActions: n, principal: r, resource: i } = e;
47
+ return s(r), l(h, r, i, n, t);
46
48
  }
47
- function S(e, t, n) {
48
- return s(e), u(h, e, t, n);
49
+ function S(e) {
50
+ let { data: t, principal: n, resource: r } = e;
51
+ return s(n), u(h, n, r, t);
49
52
  }
50
- function C(e, t, n, a) {
51
- s(e);
52
- let c = [];
53
- for (let r of h) i(r, e, t, n, a) && c.push(r);
54
- let l;
55
- for (let e of c) (!l || r(l, e)) && (l = e);
56
- let u = o(l);
53
+ function C(e) {
54
+ let { action: t, data: n, principal: a, resource: c } = e;
55
+ s(a);
56
+ let l = [];
57
+ for (let e of h) i(e, a, c, t, n) && l.push(e);
58
+ let u;
59
+ for (let e of l) (!u || r(u, e)) && (u = e);
60
+ let d = o(u);
57
61
  return {
58
- candidates: c.map((e) => ({
62
+ candidates: l.map((e) => ({
59
63
  index: e.index,
60
64
  priority: e.priority,
61
65
  rule: e.rule,
62
66
  score: e.score,
63
- won: e === l
67
+ won: e === u
64
68
  })),
65
- decision: u
69
+ decision: d
66
70
  };
67
71
  }
68
72
  function w(e) {
@@ -73,11 +77,16 @@ function d(d = [], f = {}) {
73
77
  roles: [...e.roles]
74
78
  };
75
79
  return {
76
- allowedActions: (e, n, r) => l(h, t, e, n, r),
80
+ allowedActions: (e) => l(h, t, e.resource, e.knownActions, e.data),
77
81
  checkAll: (e) => e.length === 0 ? [] : y(t, e),
78
- explain: (e, n, r) => _(t, e, n, r),
79
- rulesInScope: (e, n) => u(h, t, e, n),
80
- trace: (e, n, r) => C(t, e, n, r)
82
+ explain: (e) => _(t, e.resource, e.action, e.data),
83
+ rulesInScope: (e) => u(h, t, e.resource, e.data),
84
+ trace: (e) => C({
85
+ action: e.action,
86
+ data: e.data,
87
+ principal: t,
88
+ resource: e.resource
89
+ })
81
90
  };
82
91
  }
83
92
  let T;
@@ -1 +1 @@
1
- {"version":3,"file":"factory.js","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type {\n BoundWard,\n Principal,\n UserPrincipal,\n Ward,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionResult,\n WardOptions,\n WardRule,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\n\n// ---------------------------------------------------------------------------\n// Shared loop cores (validation-free; used by both public API and forUser)\n// ---------------------------------------------------------------------------\n\nfunction coreAllowedActions<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data: TData | undefined,\n): TAction[] {\n const seen = new Set<TAction>();\n const result: TAction[] = [];\n\n for (const action of knownActions) {\n if (seen.has(action)) continue;\n\n seen.add(action);\n\n const winner = pickWinner(entries, principal, resource, action, data);\n\n if (winner?.rule.effect === 'allow') result.push(action);\n }\n\n return result;\n}\n\nfunction coreRulesInScope<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n data: TData | undefined,\n): CompiledEntry<TAction, TData>['rule'][] {\n const skipPredicate = data === undefined;\n const result: CompiledEntry<TAction, TData>['rule'][] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n const { logger, maxConflicts = Infinity } = options;\n const entries = rules.map((rule, i) => compileEntry(rule, i));\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function fireLogger(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n decision: WardDecision<TAction, TData>,\n ): void {\n if (!logger) return;\n\n logger({ ...decision, action, data, principal, resource } as Parameters<typeof logger>[0]);\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n fireLogger(principal, resource, action, data, decision);\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n function explain(\n principal: Principal,\n resource: string,\n action: TAction,\n data?: TData,\n ): WardDecision<TAction, TData> {\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data?: TData,\n ): TAction[] {\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(\n principal: Principal,\n resource: string,\n data?: TData,\n ): ReadonlyArray<Readonly<WardRule<TAction, TData>>> {\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(principal: Principal, resource: string, action: TAction, data?: TData): WardTrace<TAction, TData> {\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (resource, knownActions, data?) =>\n coreAllowedActions(entries, snap, resource, knownActions, data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (resource, action, data?) => evaluateAndLog(snap, resource, action, data),\n rulesInScope: (resource, data?) => coreRulesInScope(entries, snap, resource, data),\n trace: (resource, action, data?) => trace(snap, resource, action, data),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, trace };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"factory.js","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type {\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n BoundWard,\n Principal,\n UserPrincipal,\n WardAllowedActionsInput,\n Ward,\n WardCheck,\n WardConflict,\n WardDecisionInput,\n WardDecision,\n WardDecisionResult,\n WardOptions,\n WardRulesInScopeInput,\n WardRule,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\n\n// ---------------------------------------------------------------------------\n// Shared loop cores (validation-free; used by both public API and forUser)\n// ---------------------------------------------------------------------------\n\nfunction coreAllowedActions<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data: TData | undefined,\n): TAction[] {\n const seen = new Set<TAction>();\n const result: TAction[] = [];\n\n for (const action of knownActions) {\n if (seen.has(action)) continue;\n\n seen.add(action);\n\n const winner = pickWinner(entries, principal, resource, action, data);\n\n if (winner?.rule.effect === 'allow') result.push(action);\n }\n\n return result;\n}\n\nfunction coreRulesInScope<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n data: TData | undefined,\n): CompiledEntry<TAction, TData>['rule'][] {\n const skipPredicate = data === undefined;\n const result: CompiledEntry<TAction, TData>['rule'][] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n const { logger, maxConflicts = Infinity } = options;\n const entries = rules.map((rule, i) => compileEntry(rule, i));\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function fireLogger(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n decision: WardDecision<TAction, TData>,\n ): void {\n if (!logger) return;\n\n logger({ ...decision, action, data, principal, resource } as Parameters<typeof logger>[0]);\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n fireLogger(principal, resource, action, data, decision);\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n // Request objects avoid call-site ambiguity between resource/action/data and\n // make later API growth additive instead of positional-breaking.\n\n function explain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[] {\n const { data, knownActions, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(input: WardRulesInScopeInput<TData>): ReadonlyArray<Readonly<WardRule<TAction, TData>>> {\n const { data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (input: BoundWardAllowedActionsInput<TAction, TData>) =>\n coreAllowedActions(entries, snap, input.resource, input.knownActions, input.data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (input: BoundWardDecisionInput<TAction, TData>) =>\n evaluateAndLog(snap, input.resource, input.action, input.data),\n rulesInScope: (input: BoundWardRulesInScopeInput<TData>) =>\n coreRulesInScope(entries, snap, input.resource, input.data),\n trace: (input: BoundWardDecisionInput<TAction, TData>) =>\n trace({ action: input.action, data: input.data, principal: snap, resource: input.resource }),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, trace };\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -4,6 +4,6 @@ export { WardConfigError, WardError, WardPredicateError } from './errors';
4
4
  export { createWard } from './factory';
5
5
  export { guardRequest, guardRequestWith } from './middleware';
6
6
  export { matchesPattern, patternCovers } from './resource';
7
- export type { BoundWard, ConflictKind, Principal, RuleContext, UserPrincipal, Ward, WardCheck, WardConflict, WardDecision, WardDecisionResult, WardLoggerContext, WardOptions, WardPredicate, WardRule, WardTrace, WardTraceCandidate, } from './types';
8
- export type { GuardResult, PrincipalExtractor, WardRequest } from './middleware';
7
+ export type { BoundWardAllowedActionsInput, BoundWardDecisionInput, BoundWardRulesInScopeInput, BoundWard, ConflictKind, Principal, RuleContext, UserPrincipal, WardAllowedActionsInput, Ward, WardCheck, WardConflict, WardDecisionInput, WardDecision, WardDecisionResult, WardLoggerContext, WardOptions, WardPredicate, WardRulesInScopeInput, WardRule, WardTrace, WardTraceCandidate, } from './types';
8
+ export type { GuardRequestInput, GuardRequestWithInput, GuardResult, PrincipalExtractor, WardRequest, } from './middleware';
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3D,YAAY,EACV,SAAS,EACT,YAAY,EACZ,SAAS,EACT,WAAW,EACX,aAAa,EACb,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,QAAQ,EACR,SAAS,EACT,kBAAkB,GACnB,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3D,YAAY,EACV,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,EAC1B,SAAS,EACT,YAAY,EACZ,SAAS,EACT,WAAW,EACX,aAAa,EACb,uBAAuB,EACvB,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,qBAAqB,EACrB,QAAQ,EACR,SAAS,EACT,kBAAkB,GACnB,MAAM,SAAS,CAAC;AACjB,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,WAAW,EACX,kBAAkB,EAClB,WAAW,GACZ,MAAM,cAAc,CAAC"}
@@ -1,2 +1,2 @@
1
- function e(e,t,n,r,i){let a=e.explain(t,n,r,i);return a.allowed?{granted:!0,principal:t}:{decision:a,granted:!1,principal:t,reason:a.reason}}async function t(t,n,r,i,a,o){return e(t,await r(n),i,a,o)}exports.guardRequest=e,exports.guardRequestWith=t;
1
+ function e(e){let t=e.ward.explain({action:e.action,data:e.data,principal:e.principal,resource:e.resource});return t.allowed?{granted:!0,principal:e.principal}:{decision:t,granted:!1,principal:e.principal,reason:t.reason}}async function t(t){let n=await t.extractPrincipal(t.req);return e({action:t.action,data:t.data,principal:n,resource:t.resource,ward:t.ward})}exports.guardRequest=e,exports.guardRequestWith=t;
2
2
  //# sourceMappingURL=middleware.cjs.map