@vielzeug/ward 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,11 +11,11 @@ pnpm add @vielzeug/ward
11
11
  ## Quick Start
12
12
 
13
13
  ```ts
14
- import { ANONYMOUS, WILDCARD, allow, createWard, deny, owns } from '@vielzeug/ward';
14
+ import { ANONYMOUS, WILDCARD, allow, createWard, deny, predicate } from '@vielzeug/ward';
15
15
 
16
16
  const ward = createWard<'read' | 'update', { authorId: string }>([
17
17
  allow([ANONYMOUS, 'viewer'], 'posts', ['read']),
18
- allow('editor', 'posts', ['update'], { when: owns('authorId') }),
18
+ allow('editor', 'posts', ['update'], { when: predicate.owns('authorId') }),
19
19
  deny('blocked', WILDCARD, [WILDCARD], { priority: 100 }),
20
20
  ]);
21
21
 
@@ -55,5 +55,5 @@ bound.explain({ resource: 'posts', action: 'update', data: { authorId: 'u2' } })
55
55
  6. Use `explain()` directly at request boundaries instead of middleware wrappers.
56
56
  7. Subscribe to decision events with `tap()` for logging and diagnostics:
57
57
  ```ts
58
- ward.tap((event) => console.debug(`ward:${event.type}`, event.decision));
58
+ ward.tap((event) => console.debug('ward:decision', event.decision));
59
59
  ```
@@ -1 +1 @@
1
- {"version":3,"file":"_compile.cjs","names":[],"sources":["../src/_compile.ts"],"sourcesContent":["import { WILDCARD } from './constants';\nimport { WardConfigError } from './errors';\nimport type { WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// Internal types (shared across modules)\n// ---------------------------------------------------------------------------\n\n/** Normalized compiled rule — role always readonly string[], priority always number. */\nexport type CompiledRule<TAction extends string, TData> = Readonly<{\n action: TAction | typeof WILDCARD;\n effect: 'allow' | 'deny';\n priority: number;\n resource: string | typeof WILDCARD;\n role: readonly string[];\n when?: WardRule<TAction, TData>['when'];\n}>;\n\n/** A compiled entry stores the normalized rule plus pre-computed lookup values. */\nexport type CompiledEntry<TAction extends string, TData> = {\n /** Deny bonus (1 for deny, 0 for allow): tiebreaker when priority and score match. */\n denyBonus: 0 | 1;\n /** Original rule index, used in predicate error messages. */\n index: number;\n /** Resolved priority (defaults to 0 when not authored). */\n priority: number;\n /** Normalized roles array (always an array). */\n roles: readonly string[];\n /** The normalized, frozen compiled rule. */\n rule: CompiledRule<TAction, TData>;\n /** Specificity score (0–5): roleScore(0|1) + resourceScore(0|1|2) + actionScore(0|1|2). */\n score: number;\n};\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport function validateRuleInput<TAction extends string, TData>(rule: WardRule<TAction, TData>, index: number): void {\n const at = `Rule[${index}]`;\n const roles = Array.isArray(rule.role) ? rule.role : [rule.role];\n\n if (roles.length === 0 || roles.some((r) => typeof r !== 'string' || !r.trim())) {\n throw new WardConfigError(`${at}.role must be a non-empty string or non-empty array of strings`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if ((rule.resource as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.resource '${String(rule.resource)}' ends with ':' — did you mean '${String(rule.resource)}*'?`,\n );\n }\n\n if (typeof rule.action !== 'string' || !(rule.action as string).trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if ((rule.action as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.action '${String(rule.action)}' ends with ':' — did you mean '${String(rule.action)}*'?`,\n );\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.priority !== undefined && (typeof rule.priority !== 'number' || !Number.isFinite(rule.priority))) {\n throw new WardConfigError(`${at}.priority must be a finite number`);\n }\n\n if (rule.when !== undefined && typeof rule.when !== 'function') {\n throw new WardConfigError(`${at}.when must be a function`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Specificity\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the specificity score for a single pattern field:\n * - 0: global wildcard (`*`)\n * - 1: namespace wildcard (e.g. `posts:*` or `read:*`)\n * - 2: exact string\n */\nexport function patternScore(pattern: string): number {\n if (pattern === WILDCARD) return 0;\n\n if (pattern.endsWith(':*')) return 1;\n\n return 2;\n}\n\n/**\n * Specificity: role(0|1) + resource(0|1|2) + action(0|1|2) = max 5.\n * Higher score = more specific = wins ties in priority.\n */\nfunction specificity<TAction extends string, TData>(rule: CompiledRule<TAction, TData>): number {\n const roleScore = rule.role.includes(WILDCARD) ? 0 : 1;\n\n return roleScore + patternScore(rule.resource as string) + patternScore(rule.action as string);\n}\n\n// ---------------------------------------------------------------------------\n// Compilation\n// ---------------------------------------------------------------------------\n\nexport function compileEntry<TAction extends string, TData>(\n input: WardRule<TAction, TData>,\n index: number,\n): CompiledEntry<TAction, TData> {\n validateRuleInput(input, index);\n\n const rawRoles = Array.isArray(input.role) ? [...input.role] : [input.role];\n const roles: readonly string[] = Object.freeze([...new Set(rawRoles)]);\n\n const rule = Object.freeze({\n action: input.action,\n effect: input.effect,\n priority: input.priority ?? 0,\n resource: input.resource,\n role: roles,\n ...(input.when !== undefined ? { when: input.when } : {}),\n }) as CompiledRule<TAction, TData>;\n\n const score = specificity(rule);\n const priority = rule.priority;\n const denyBonus: 0 | 1 = rule.effect === 'deny' ? 1 : 0;\n\n return { denyBonus, index, priority, roles, rule, score };\n}\n"],"mappings":"2DAsCA,SAAgB,EAAiD,EAAgC,EAAqB,CACpH,IAAM,EAAK,QAAQ,EAAM,GACnB,EAAQ,MAAM,QAAQ,EAAK,IAAI,EAAI,EAAK,KAAO,CAAC,EAAK,IAAI,EAE/D,GAAI,EAAM,SAAW,GAAK,EAAM,KAAM,GAAM,OAAO,GAAM,UAAY,CAAC,EAAE,KAAK,CAAC,EAC5E,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,+DAA+D,EAGjG,GAAI,OAAO,EAAK,UAAa,UAAY,CAAC,EAAK,SAAS,KAAK,EAC3D,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,qCAAqC,EAGvE,GAAK,EAAK,SAAoB,SAAS,GAAG,EACxC,MAAM,IAAI,EAAA,gBACR,GAAG,EAAG,aAAa,OAAO,EAAK,QAAQ,EAAE,kCAAkC,OAAO,EAAK,QAAQ,EAAE,IACnG,EAGF,GAAI,OAAO,EAAK,QAAW,UAAY,CAAE,EAAK,OAAkB,KAAK,EACnE,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,mCAAmC,EAGrE,GAAK,EAAK,OAAkB,SAAS,GAAG,EACtC,MAAM,IAAI,EAAA,gBACR,GAAG,EAAG,WAAW,OAAO,EAAK,MAAM,EAAE,kCAAkC,OAAO,EAAK,MAAM,EAAE,IAC7F,EAGF,GAAI,EAAK,SAAW,SAAW,EAAK,SAAW,OAC7C,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,kCAAkC,EAGpE,GAAI,EAAK,WAAa,IAAA,KAAc,OAAO,EAAK,UAAa,UAAY,CAAC,OAAO,SAAS,EAAK,QAAQ,GACrG,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,kCAAkC,EAGpE,GAAI,EAAK,OAAS,IAAA,IAAa,OAAO,EAAK,MAAS,WAClD,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,yBAAyB,CAE7D,CAYA,SAAgB,EAAa,EAAyB,CAKpD,OAJI,IAAA,IAA6B,EAE7B,EAAQ,SAAS,IAAI,EAAU,EAE5B,CACT,CAMA,SAAS,EAA2C,EAA4C,CAG9F,MAFkB,IAAK,KAAK,SAAA,GAAiB,EAE1B,EAAa,EAAK,QAAkB,EAAI,EAAa,EAAK,MAAgB,CAC/F,CAMA,SAAgB,EACd,EACA,EAC+B,CAC/B,EAAkB,EAAO,CAAK,EAE9B,IAAM,EAAW,MAAM,QAAQ,EAAM,IAAI,EAAI,CAAC,GAAG,EAAM,IAAI,EAAI,CAAC,EAAM,IAAI,EACpE,EAA2B,OAAO,OAAO,CAAC,GAAG,IAAI,IAAI,CAAQ,CAAC,CAAC,EAE/D,EAAO,OAAO,OAAO,CACzB,OAAQ,EAAM,OACd,OAAQ,EAAM,OACd,SAAU,EAAM,UAAY,EAC5B,SAAU,EAAM,SAChB,KAAM,EACN,GAAI,EAAM,OAAS,IAAA,GAAmC,CAAC,EAAxB,CAAE,KAAM,EAAM,IAAK,CACpD,CAAC,EAEK,EAAQ,EAAY,CAAI,EACxB,EAAW,EAAK,SAGtB,MAAO,CAAE,UAFgB,IAAK,SAAW,QAErB,QAAO,WAAU,QAAO,OAAM,OAAM,CAC1D"}
1
+ {"version":3,"file":"_compile.cjs","names":[],"sources":["../src/_compile.ts"],"sourcesContent":["import { WILDCARD } from './constants';\nimport { WardConfigError } from './errors';\nimport type { NormalizedWardRule, WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// Internal types (shared across modules)\n// ---------------------------------------------------------------------------\n\n/** A compiled entry stores the normalized rule plus pre-computed lookup values. */\nexport type CompiledEntry<TAction extends string, TData> = {\n /** Deny bonus (1 for deny, 0 for allow): tiebreaker when priority and score match. */\n denyBonus: 0 | 1;\n /** Original rule index, used in predicate error messages. */\n index: number;\n /** Resolved priority (defaults to 0 when not authored). */\n priority: number;\n /** Normalized roles array (always an array). */\n roles: readonly string[];\n /** The normalized, frozen compiled rule. */\n rule: Readonly<NormalizedWardRule<TAction, TData>>;\n /** Specificity score (0–5): roleScore(0|1) + resourceScore(0|1|2) + actionScore(0|1|2). */\n score: number;\n};\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport function validateRuleInput<TAction extends string, TData>(rule: WardRule<TAction, TData>, index: number): void {\n const at = `Rule[${index}]`;\n const roles = Array.isArray(rule.role) ? rule.role : [rule.role];\n\n if (roles.length === 0 || roles.some((r) => typeof r !== 'string' || !r.trim())) {\n throw new WardConfigError(`${at}.role must be a non-empty string or non-empty array of strings`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if ((rule.resource as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.resource '${String(rule.resource)}' ends with ':' — did you mean '${String(rule.resource)}*'?`,\n );\n }\n\n if (typeof rule.action !== 'string' || !(rule.action as string).trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if ((rule.action as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.action '${String(rule.action)}' ends with ':' — did you mean '${String(rule.action)}*'?`,\n );\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.priority !== undefined && (typeof rule.priority !== 'number' || !Number.isFinite(rule.priority))) {\n throw new WardConfigError(`${at}.priority must be a finite number`);\n }\n\n if (rule.when !== undefined && typeof rule.when !== 'function') {\n throw new WardConfigError(`${at}.when must be a function`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Specificity\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the specificity score for a single pattern field:\n * - 0: global wildcard (`*`)\n * - 1: namespace wildcard (e.g. `posts:*` or `read:*`)\n * - 2: exact string\n */\nexport function patternScore(pattern: string): number {\n if (pattern === WILDCARD) return 0;\n\n if (pattern.endsWith(':*')) return 1;\n\n return 2;\n}\n\n/**\n * Specificity: role(0|1) + resource(0|1|2) + action(0|1|2) = max 5.\n * Higher score = more specific = wins ties in priority.\n */\nfunction specificity<TAction extends string, TData>(rule: Readonly<NormalizedWardRule<TAction, TData>>): number {\n const roleScore = rule.role.includes(WILDCARD) ? 0 : 1;\n\n return roleScore + patternScore(rule.resource as string) + patternScore(rule.action as string);\n}\n\n// ---------------------------------------------------------------------------\n// Compilation\n// ---------------------------------------------------------------------------\n\nexport function compileEntry<TAction extends string, TData>(\n input: WardRule<TAction, TData>,\n index: number,\n): CompiledEntry<TAction, TData> {\n validateRuleInput(input, index);\n\n const rawRoles = Array.isArray(input.role) ? [...input.role] : [input.role];\n const roles: readonly string[] = Object.freeze([...new Set(rawRoles)]);\n\n const rule = Object.freeze({\n action: input.action,\n effect: input.effect,\n priority: input.priority ?? 0,\n resource: input.resource,\n role: roles,\n ...(input.when !== undefined ? { when: input.when } : {}),\n }) as Readonly<NormalizedWardRule<TAction, TData>>;\n\n const score = specificity(rule);\n const priority = rule.priority;\n const denyBonus: 0 | 1 = rule.effect === 'deny' ? 1 : 0;\n\n return { denyBonus, index, priority, roles, rule, score };\n}\n"],"mappings":"2DA4BA,SAAgB,EAAiD,EAAgC,EAAqB,CACpH,IAAM,EAAK,QAAQ,EAAM,GACnB,EAAQ,MAAM,QAAQ,EAAK,IAAI,EAAI,EAAK,KAAO,CAAC,EAAK,IAAI,EAE/D,GAAI,EAAM,SAAW,GAAK,EAAM,KAAM,GAAM,OAAO,GAAM,UAAY,CAAC,EAAE,KAAK,CAAC,EAC5E,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,+DAA+D,EAGjG,GAAI,OAAO,EAAK,UAAa,UAAY,CAAC,EAAK,SAAS,KAAK,EAC3D,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,qCAAqC,EAGvE,GAAK,EAAK,SAAoB,SAAS,GAAG,EACxC,MAAM,IAAI,EAAA,gBACR,GAAG,EAAG,aAAa,OAAO,EAAK,QAAQ,EAAE,kCAAkC,OAAO,EAAK,QAAQ,EAAE,IACnG,EAGF,GAAI,OAAO,EAAK,QAAW,UAAY,CAAE,EAAK,OAAkB,KAAK,EACnE,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,mCAAmC,EAGrE,GAAK,EAAK,OAAkB,SAAS,GAAG,EACtC,MAAM,IAAI,EAAA,gBACR,GAAG,EAAG,WAAW,OAAO,EAAK,MAAM,EAAE,kCAAkC,OAAO,EAAK,MAAM,EAAE,IAC7F,EAGF,GAAI,EAAK,SAAW,SAAW,EAAK,SAAW,OAC7C,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,kCAAkC,EAGpE,GAAI,EAAK,WAAa,IAAA,KAAc,OAAO,EAAK,UAAa,UAAY,CAAC,OAAO,SAAS,EAAK,QAAQ,GACrG,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,kCAAkC,EAGpE,GAAI,EAAK,OAAS,IAAA,IAAa,OAAO,EAAK,MAAS,WAClD,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,yBAAyB,CAE7D,CAYA,SAAgB,EAAa,EAAyB,CAKpD,OAJI,IAAA,IAA6B,EAE7B,EAAQ,SAAS,IAAI,EAAU,EAE5B,CACT,CAMA,SAAS,EAA2C,EAA4D,CAG9G,MAFkB,IAAK,KAAK,SAAA,GAAiB,EAE1B,EAAa,EAAK,QAAkB,EAAI,EAAa,EAAK,MAAgB,CAC/F,CAMA,SAAgB,EACd,EACA,EAC+B,CAC/B,EAAkB,EAAO,CAAK,EAE9B,IAAM,EAAW,MAAM,QAAQ,EAAM,IAAI,EAAI,CAAC,GAAG,EAAM,IAAI,EAAI,CAAC,EAAM,IAAI,EACpE,EAA2B,OAAO,OAAO,CAAC,GAAG,IAAI,IAAI,CAAQ,CAAC,CAAC,EAE/D,EAAO,OAAO,OAAO,CACzB,OAAQ,EAAM,OACd,OAAQ,EAAM,OACd,SAAU,EAAM,UAAY,EAC5B,SAAU,EAAM,SAChB,KAAM,EACN,GAAI,EAAM,OAAS,IAAA,GAAmC,CAAC,EAAxB,CAAE,KAAM,EAAM,IAAK,CACpD,CAAC,EAEK,EAAQ,EAAY,CAAI,EACxB,EAAW,EAAK,SAGtB,MAAO,CAAE,UAFgB,IAAK,SAAW,QAErB,QAAO,WAAU,QAAO,OAAM,OAAM,CAC1D"}
@@ -1,14 +1,4 @@
1
- import { WILDCARD } from './constants';
2
- import type { WardRule } from './types';
3
- /** Normalized compiled rule — role always readonly string[], priority always number. */
4
- export type CompiledRule<TAction extends string, TData> = Readonly<{
5
- action: TAction | typeof WILDCARD;
6
- effect: 'allow' | 'deny';
7
- priority: number;
8
- resource: string | typeof WILDCARD;
9
- role: readonly string[];
10
- when?: WardRule<TAction, TData>['when'];
11
- }>;
1
+ import type { NormalizedWardRule, WardRule } from './types';
12
2
  /** A compiled entry stores the normalized rule plus pre-computed lookup values. */
13
3
  export type CompiledEntry<TAction extends string, TData> = {
14
4
  /** Deny bonus (1 for deny, 0 for allow): tiebreaker when priority and score match. */
@@ -20,7 +10,7 @@ export type CompiledEntry<TAction extends string, TData> = {
20
10
  /** Normalized roles array (always an array). */
21
11
  roles: readonly string[];
22
12
  /** The normalized, frozen compiled rule. */
23
- rule: CompiledRule<TAction, TData>;
13
+ rule: Readonly<NormalizedWardRule<TAction, TData>>;
24
14
  /** Specificity score (0–5): roleScore(0|1) + resourceScore(0|1|2) + actionScore(0|1|2). */
25
15
  score: number;
26
16
  };
@@ -1 +1 @@
1
- {"version":3,"file":"_compile.d.ts","sourceRoot":"","sources":["../src/_compile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAMxC,wFAAwF;AACxF,MAAM,MAAM,YAAY,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,IAAI,QAAQ,CAAC;IACjE,MAAM,EAAE,OAAO,GAAG,OAAO,QAAQ,CAAC;IAClC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,CAAC;IACnC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;CACzC,CAAC,CAAC;AAEH,mFAAmF;AACnF,MAAM,MAAM,aAAa,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,IAAI;IACzD,sFAAsF;IACtF,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;IACjB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,4CAA4C;IAC5C,IAAI,EAAE,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACnC,2FAA2F;IAC3F,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAMF,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAuCpH;AAMD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMpD;AAgBD,wBAAgB,YAAY,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACxD,KAAK,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAC/B,KAAK,EAAE,MAAM,GACZ,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,CAoB/B"}
1
+ {"version":3,"file":"_compile.d.ts","sourceRoot":"","sources":["../src/_compile.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM5D,mFAAmF;AACnF,MAAM,MAAM,aAAa,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,IAAI;IACzD,sFAAsF;IACtF,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;IACjB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,4CAA4C;IAC5C,IAAI,EAAE,QAAQ,CAAC,kBAAkB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;IACnD,2FAA2F;IAC3F,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAMF,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAuCpH;AAMD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMpD;AAgBD,wBAAgB,YAAY,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACxD,KAAK,EAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAC/B,KAAK,EAAE,MAAM,GACZ,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,CAoB/B"}
@@ -1 +1 @@
1
- {"version":3,"file":"_compile.js","names":[],"sources":["../src/_compile.ts"],"sourcesContent":["import { WILDCARD } from './constants';\nimport { WardConfigError } from './errors';\nimport type { WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// Internal types (shared across modules)\n// ---------------------------------------------------------------------------\n\n/** Normalized compiled rule — role always readonly string[], priority always number. */\nexport type CompiledRule<TAction extends string, TData> = Readonly<{\n action: TAction | typeof WILDCARD;\n effect: 'allow' | 'deny';\n priority: number;\n resource: string | typeof WILDCARD;\n role: readonly string[];\n when?: WardRule<TAction, TData>['when'];\n}>;\n\n/** A compiled entry stores the normalized rule plus pre-computed lookup values. */\nexport type CompiledEntry<TAction extends string, TData> = {\n /** Deny bonus (1 for deny, 0 for allow): tiebreaker when priority and score match. */\n denyBonus: 0 | 1;\n /** Original rule index, used in predicate error messages. */\n index: number;\n /** Resolved priority (defaults to 0 when not authored). */\n priority: number;\n /** Normalized roles array (always an array). */\n roles: readonly string[];\n /** The normalized, frozen compiled rule. */\n rule: CompiledRule<TAction, TData>;\n /** Specificity score (0–5): roleScore(0|1) + resourceScore(0|1|2) + actionScore(0|1|2). */\n score: number;\n};\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport function validateRuleInput<TAction extends string, TData>(rule: WardRule<TAction, TData>, index: number): void {\n const at = `Rule[${index}]`;\n const roles = Array.isArray(rule.role) ? rule.role : [rule.role];\n\n if (roles.length === 0 || roles.some((r) => typeof r !== 'string' || !r.trim())) {\n throw new WardConfigError(`${at}.role must be a non-empty string or non-empty array of strings`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if ((rule.resource as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.resource '${String(rule.resource)}' ends with ':' — did you mean '${String(rule.resource)}*'?`,\n );\n }\n\n if (typeof rule.action !== 'string' || !(rule.action as string).trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if ((rule.action as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.action '${String(rule.action)}' ends with ':' — did you mean '${String(rule.action)}*'?`,\n );\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.priority !== undefined && (typeof rule.priority !== 'number' || !Number.isFinite(rule.priority))) {\n throw new WardConfigError(`${at}.priority must be a finite number`);\n }\n\n if (rule.when !== undefined && typeof rule.when !== 'function') {\n throw new WardConfigError(`${at}.when must be a function`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Specificity\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the specificity score for a single pattern field:\n * - 0: global wildcard (`*`)\n * - 1: namespace wildcard (e.g. `posts:*` or `read:*`)\n * - 2: exact string\n */\nexport function patternScore(pattern: string): number {\n if (pattern === WILDCARD) return 0;\n\n if (pattern.endsWith(':*')) return 1;\n\n return 2;\n}\n\n/**\n * Specificity: role(0|1) + resource(0|1|2) + action(0|1|2) = max 5.\n * Higher score = more specific = wins ties in priority.\n */\nfunction specificity<TAction extends string, TData>(rule: CompiledRule<TAction, TData>): number {\n const roleScore = rule.role.includes(WILDCARD) ? 0 : 1;\n\n return roleScore + patternScore(rule.resource as string) + patternScore(rule.action as string);\n}\n\n// ---------------------------------------------------------------------------\n// Compilation\n// ---------------------------------------------------------------------------\n\nexport function compileEntry<TAction extends string, TData>(\n input: WardRule<TAction, TData>,\n index: number,\n): CompiledEntry<TAction, TData> {\n validateRuleInput(input, index);\n\n const rawRoles = Array.isArray(input.role) ? [...input.role] : [input.role];\n const roles: readonly string[] = Object.freeze([...new Set(rawRoles)]);\n\n const rule = Object.freeze({\n action: input.action,\n effect: input.effect,\n priority: input.priority ?? 0,\n resource: input.resource,\n role: roles,\n ...(input.when !== undefined ? { when: input.when } : {}),\n }) as CompiledRule<TAction, TData>;\n\n const score = specificity(rule);\n const priority = rule.priority;\n const denyBonus: 0 | 1 = rule.effect === 'deny' ? 1 : 0;\n\n return { denyBonus, index, priority, roles, rule, score };\n}\n"],"mappings":";;;AAsCA,SAAgB,EAAiD,GAAgC,GAAqB;CACpH,IAAM,IAAK,QAAQ,EAAM,IACnB,IAAQ,MAAM,QAAQ,EAAK,IAAI,IAAI,EAAK,OAAO,CAAC,EAAK,IAAI;CAE/D,IAAI,EAAM,WAAW,KAAK,EAAM,MAAM,MAAM,OAAO,KAAM,YAAY,CAAC,EAAE,KAAK,CAAC,GAC5E,MAAM,IAAI,EAAgB,GAAG,EAAG,+DAA+D;CAGjG,IAAI,OAAO,EAAK,YAAa,YAAY,CAAC,EAAK,SAAS,KAAK,GAC3D,MAAM,IAAI,EAAgB,GAAG,EAAG,qCAAqC;CAGvE,IAAK,EAAK,SAAoB,SAAS,GAAG,GACxC,MAAM,IAAI,EACR,GAAG,EAAG,aAAa,OAAO,EAAK,QAAQ,EAAE,kCAAkC,OAAO,EAAK,QAAQ,EAAE,IACnG;CAGF,IAAI,OAAO,EAAK,UAAW,YAAY,CAAE,EAAK,OAAkB,KAAK,GACnE,MAAM,IAAI,EAAgB,GAAG,EAAG,mCAAmC;CAGrE,IAAK,EAAK,OAAkB,SAAS,GAAG,GACtC,MAAM,IAAI,EACR,GAAG,EAAG,WAAW,OAAO,EAAK,MAAM,EAAE,kCAAkC,OAAO,EAAK,MAAM,EAAE,IAC7F;CAGF,IAAI,EAAK,WAAW,WAAW,EAAK,WAAW,QAC7C,MAAM,IAAI,EAAgB,GAAG,EAAG,kCAAkC;CAGpE,IAAI,EAAK,aAAa,KAAA,MAAc,OAAO,EAAK,YAAa,YAAY,CAAC,OAAO,SAAS,EAAK,QAAQ,IACrG,MAAM,IAAI,EAAgB,GAAG,EAAG,kCAAkC;CAGpE,IAAI,EAAK,SAAS,KAAA,KAAa,OAAO,EAAK,QAAS,YAClD,MAAM,IAAI,EAAgB,GAAG,EAAG,yBAAyB;AAE7D;AAYA,SAAgB,EAAa,GAAyB;CAKpD,OAJI,MAAA,MAA6B,IAE7B,EAAQ,SAAS,IAAI,IAAU,IAE5B;AACT;AAMA,SAAS,EAA2C,GAA4C;CAG9F,OAFkB,IAAK,KAAK,SAAA,GAAiB,IAE1B,EAAa,EAAK,QAAkB,IAAI,EAAa,EAAK,MAAgB;AAC/F;AAMA,SAAgB,EACd,GACA,GAC+B;CAC/B,EAAkB,GAAO,CAAK;CAE9B,IAAM,IAAW,MAAM,QAAQ,EAAM,IAAI,IAAI,CAAC,GAAG,EAAM,IAAI,IAAI,CAAC,EAAM,IAAI,GACpE,IAA2B,OAAO,OAAO,CAAC,GAAG,IAAI,IAAI,CAAQ,CAAC,CAAC,GAE/D,IAAO,OAAO,OAAO;EACzB,QAAQ,EAAM;EACd,QAAQ,EAAM;EACd,UAAU,EAAM,YAAY;EAC5B,UAAU,EAAM;EAChB,MAAM;EACN,GAAI,EAAM,SAAS,KAAA,IAAmC,CAAC,IAAxB,EAAE,MAAM,EAAM,KAAK;CACpD,CAAC,GAEK,IAAQ,EAAY,CAAI,GACxB,IAAW,EAAK;CAGtB,OAAO;EAAE,WAFgB,IAAK,WAAW;EAErB;EAAO;EAAU;EAAO;EAAM;CAAM;AAC1D"}
1
+ {"version":3,"file":"_compile.js","names":[],"sources":["../src/_compile.ts"],"sourcesContent":["import { WILDCARD } from './constants';\nimport { WardConfigError } from './errors';\nimport type { NormalizedWardRule, WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// Internal types (shared across modules)\n// ---------------------------------------------------------------------------\n\n/** A compiled entry stores the normalized rule plus pre-computed lookup values. */\nexport type CompiledEntry<TAction extends string, TData> = {\n /** Deny bonus (1 for deny, 0 for allow): tiebreaker when priority and score match. */\n denyBonus: 0 | 1;\n /** Original rule index, used in predicate error messages. */\n index: number;\n /** Resolved priority (defaults to 0 when not authored). */\n priority: number;\n /** Normalized roles array (always an array). */\n roles: readonly string[];\n /** The normalized, frozen compiled rule. */\n rule: Readonly<NormalizedWardRule<TAction, TData>>;\n /** Specificity score (0–5): roleScore(0|1) + resourceScore(0|1|2) + actionScore(0|1|2). */\n score: number;\n};\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport function validateRuleInput<TAction extends string, TData>(rule: WardRule<TAction, TData>, index: number): void {\n const at = `Rule[${index}]`;\n const roles = Array.isArray(rule.role) ? rule.role : [rule.role];\n\n if (roles.length === 0 || roles.some((r) => typeof r !== 'string' || !r.trim())) {\n throw new WardConfigError(`${at}.role must be a non-empty string or non-empty array of strings`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if ((rule.resource as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.resource '${String(rule.resource)}' ends with ':' — did you mean '${String(rule.resource)}*'?`,\n );\n }\n\n if (typeof rule.action !== 'string' || !(rule.action as string).trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if ((rule.action as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.action '${String(rule.action)}' ends with ':' — did you mean '${String(rule.action)}*'?`,\n );\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.priority !== undefined && (typeof rule.priority !== 'number' || !Number.isFinite(rule.priority))) {\n throw new WardConfigError(`${at}.priority must be a finite number`);\n }\n\n if (rule.when !== undefined && typeof rule.when !== 'function') {\n throw new WardConfigError(`${at}.when must be a function`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Specificity\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the specificity score for a single pattern field:\n * - 0: global wildcard (`*`)\n * - 1: namespace wildcard (e.g. `posts:*` or `read:*`)\n * - 2: exact string\n */\nexport function patternScore(pattern: string): number {\n if (pattern === WILDCARD) return 0;\n\n if (pattern.endsWith(':*')) return 1;\n\n return 2;\n}\n\n/**\n * Specificity: role(0|1) + resource(0|1|2) + action(0|1|2) = max 5.\n * Higher score = more specific = wins ties in priority.\n */\nfunction specificity<TAction extends string, TData>(rule: Readonly<NormalizedWardRule<TAction, TData>>): number {\n const roleScore = rule.role.includes(WILDCARD) ? 0 : 1;\n\n return roleScore + patternScore(rule.resource as string) + patternScore(rule.action as string);\n}\n\n// ---------------------------------------------------------------------------\n// Compilation\n// ---------------------------------------------------------------------------\n\nexport function compileEntry<TAction extends string, TData>(\n input: WardRule<TAction, TData>,\n index: number,\n): CompiledEntry<TAction, TData> {\n validateRuleInput(input, index);\n\n const rawRoles = Array.isArray(input.role) ? [...input.role] : [input.role];\n const roles: readonly string[] = Object.freeze([...new Set(rawRoles)]);\n\n const rule = Object.freeze({\n action: input.action,\n effect: input.effect,\n priority: input.priority ?? 0,\n resource: input.resource,\n role: roles,\n ...(input.when !== undefined ? { when: input.when } : {}),\n }) as Readonly<NormalizedWardRule<TAction, TData>>;\n\n const score = specificity(rule);\n const priority = rule.priority;\n const denyBonus: 0 | 1 = rule.effect === 'deny' ? 1 : 0;\n\n return { denyBonus, index, priority, roles, rule, score };\n}\n"],"mappings":";;;AA4BA,SAAgB,EAAiD,GAAgC,GAAqB;CACpH,IAAM,IAAK,QAAQ,EAAM,IACnB,IAAQ,MAAM,QAAQ,EAAK,IAAI,IAAI,EAAK,OAAO,CAAC,EAAK,IAAI;CAE/D,IAAI,EAAM,WAAW,KAAK,EAAM,MAAM,MAAM,OAAO,KAAM,YAAY,CAAC,EAAE,KAAK,CAAC,GAC5E,MAAM,IAAI,EAAgB,GAAG,EAAG,+DAA+D;CAGjG,IAAI,OAAO,EAAK,YAAa,YAAY,CAAC,EAAK,SAAS,KAAK,GAC3D,MAAM,IAAI,EAAgB,GAAG,EAAG,qCAAqC;CAGvE,IAAK,EAAK,SAAoB,SAAS,GAAG,GACxC,MAAM,IAAI,EACR,GAAG,EAAG,aAAa,OAAO,EAAK,QAAQ,EAAE,kCAAkC,OAAO,EAAK,QAAQ,EAAE,IACnG;CAGF,IAAI,OAAO,EAAK,UAAW,YAAY,CAAE,EAAK,OAAkB,KAAK,GACnE,MAAM,IAAI,EAAgB,GAAG,EAAG,mCAAmC;CAGrE,IAAK,EAAK,OAAkB,SAAS,GAAG,GACtC,MAAM,IAAI,EACR,GAAG,EAAG,WAAW,OAAO,EAAK,MAAM,EAAE,kCAAkC,OAAO,EAAK,MAAM,EAAE,IAC7F;CAGF,IAAI,EAAK,WAAW,WAAW,EAAK,WAAW,QAC7C,MAAM,IAAI,EAAgB,GAAG,EAAG,kCAAkC;CAGpE,IAAI,EAAK,aAAa,KAAA,MAAc,OAAO,EAAK,YAAa,YAAY,CAAC,OAAO,SAAS,EAAK,QAAQ,IACrG,MAAM,IAAI,EAAgB,GAAG,EAAG,kCAAkC;CAGpE,IAAI,EAAK,SAAS,KAAA,KAAa,OAAO,EAAK,QAAS,YAClD,MAAM,IAAI,EAAgB,GAAG,EAAG,yBAAyB;AAE7D;AAYA,SAAgB,EAAa,GAAyB;CAKpD,OAJI,MAAA,MAA6B,IAE7B,EAAQ,SAAS,IAAI,IAAU,IAE5B;AACT;AAMA,SAAS,EAA2C,GAA4D;CAG9G,OAFkB,IAAK,KAAK,SAAA,GAAiB,IAE1B,EAAa,EAAK,QAAkB,IAAI,EAAa,EAAK,MAAgB;AAC/F;AAMA,SAAgB,EACd,GACA,GAC+B;CAC/B,EAAkB,GAAO,CAAK;CAE9B,IAAM,IAAW,MAAM,QAAQ,EAAM,IAAI,IAAI,CAAC,GAAG,EAAM,IAAI,IAAI,CAAC,EAAM,IAAI,GACpE,IAA2B,OAAO,OAAO,CAAC,GAAG,IAAI,IAAI,CAAQ,CAAC,CAAC,GAE/D,IAAO,OAAO,OAAO;EACzB,QAAQ,EAAM;EACd,QAAQ,EAAM;EACd,UAAU,EAAM,YAAY;EAC5B,UAAU,EAAM;EAChB,MAAM;EACN,GAAI,EAAM,SAAS,KAAA,IAAmC,CAAC,IAAxB,EAAE,MAAM,EAAM,KAAK;CACpD,CAAC,GAEK,IAAQ,EAAY,CAAI,GACxB,IAAW,EAAK;CAGtB,OAAO;EAAE,WAFgB,IAAK,WAAW;EAErB;EAAO;EAAU;EAAO;EAAM;CAAM;AAC1D"}
@@ -1 +1 @@
1
- {"version":3,"file":"_match.cjs","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\nimport type { NormalizedWardRule, Principal, UserPrincipal, WardDecision } from './types';\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 {\n allowed: false,\n reason: 'explicit-deny',\n rule: winner.rule as Readonly<NormalizedWardRule<TAction, TData>>,\n };\n }\n\n return { allowed: true, rule: winner.rule as Readonly<NormalizedWardRule<TAction, TData>> };\n}\n"],"mappings":"yFAWA,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,CAW9B,OAVK,EAED,EAAO,KAAK,SAAW,OAClB,CACL,QAAS,GACT,OAAQ,gBACR,KAAM,EAAO,IACf,EAGK,CAAE,QAAS,GAAM,KAAM,EAAO,IAAqD,EAVtE,CAAE,QAAS,GAAO,OAAQ,kBAAmB,CAWnE"}
1
+ {"version":3,"file":"_match.cjs","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\nimport type { Principal, UserPrincipal, WardDecision } from './types';\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 {\n allowed: false,\n reason: 'explicit-deny',\n rule: winner.rule,\n };\n }\n\n return { allowed: true, rule: winner.rule };\n}\n"],"mappings":"yFAWA,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,CAW9B,OAVK,EAED,EAAO,KAAK,SAAW,OAClB,CACL,QAAS,GACT,OAAQ,gBACR,KAAM,EAAO,IACf,EAGK,CAAE,QAAS,GAAM,KAAM,EAAO,IAAK,EAVtB,CAAE,QAAS,GAAO,OAAQ,kBAAmB,CAWnE"}
@@ -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;AAIhD,OAAO,KAAK,EAAsB,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAM1F,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,CAY9B"}
1
+ {"version":3,"file":"_match.d.ts","sourceRoot":"","sources":["../src/_match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIhD,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAMtE,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,CAY9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"_match.js","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\nimport type { NormalizedWardRule, Principal, UserPrincipal, WardDecision } from './types';\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 {\n allowed: false,\n reason: 'explicit-deny',\n rule: winner.rule as Readonly<NormalizedWardRule<TAction, TData>>,\n };\n }\n\n return { allowed: true, rule: winner.rule as Readonly<NormalizedWardRule<TAction, TData>> };\n}\n"],"mappings":";;;;AAWA,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;CAW9B,OAVK,IAED,EAAO,KAAK,WAAW,SAClB;EACL,SAAS;EACT,QAAQ;EACR,MAAM,EAAO;CACf,IAGK;EAAE,SAAS;EAAM,MAAM,EAAO;CAAqD,IAVtE;EAAE,SAAS;EAAO,QAAQ;CAAmB;AAWnE"}
1
+ {"version":3,"file":"_match.js","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\nimport type { Principal, UserPrincipal, WardDecision } from './types';\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 {\n allowed: false,\n reason: 'explicit-deny',\n rule: winner.rule,\n };\n }\n\n return { allowed: true, rule: winner.rule };\n}\n"],"mappings":";;;;AAWA,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;CAW9B,OAVK,IAED,EAAO,KAAK,WAAW,SAClB;EACL,SAAS;EACT,QAAQ;EACR,MAAM,EAAO;CACf,IAGK;EAAE,SAAS;EAAM,MAAM,EAAO;CAAK,IAVtB;EAAE,SAAS;EAAO,QAAQ;CAAmB;AAWnE"}
package/dist/builder.cjs CHANGED
@@ -1,2 +1,2 @@
1
- function e(e,t,n,r,i){return r.map(r=>({action:r,effect:e,...i?.priority===void 0?{}:{priority:i.priority},resource:n,role:t,...i?.when===void 0?{}:{when:i.when}}))}function t(t,n,r,i){return e(`allow`,t,n,r,i)}function n(t,n,r,i){return e(`deny`,t,n,r,i)}var r={and(...e){return t=>e.every(e=>e(t))},not(e){return t=>!e(t)},or(...e){return t=>e.some(e=>e(t))},owns(e){return({data:t,principal:n})=>{if(!t||typeof t!=`object`)return!1;let r=t;return Object.hasOwn(r,e)?r[e]===n.id:!1}}};function i(e){return r.owns(e)}exports.allow=t,exports.deny=n,exports.owns=i,exports.predicate=r,exports.ruleFor=e;
1
+ function e(e,t,n,r,i){return r.map(r=>({action:r,effect:e,...i?.priority===void 0?{}:{priority:i.priority},resource:n,role:t,...i?.when===void 0?{}:{when:i.when}}))}function t(t,n,r,i){return e(`allow`,t,n,r,i)}function n(t,n,r,i){return e(`deny`,t,n,r,i)}var r={and(...e){return t=>e.every(e=>e(t))},not(e){return t=>!e(t)},or(...e){return t=>e.some(e=>e(t))},owns(e){return({data:t,principal:n})=>{if(!t||typeof t!=`object`)return!1;let r=t;return Object.hasOwn(r,e)?r[e]===n.id:!1}}};exports.allow=t,exports.deny=n,exports.predicate=r;
2
2
  //# sourceMappingURL=builder.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"builder.cjs","names":[],"sources":["../src/builder.ts"],"sourcesContent":["import type { WILDCARD } from './constants';\nimport type { WardPredicate, WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// RuleOptions — shared options for allow/deny/ruleFor\n// ---------------------------------------------------------------------------\n\ntype RuleOptions<TData> = {\n priority?: number;\n when?: WardPredicate<TData>;\n};\n\n// ---------------------------------------------------------------------------\n// ruleFormulti-action rule factory (low-level, effect as first arg)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action for a given effect, role(s), and resource.\n *\n * Prefer `allow()` or `deny()` for ergonomic rule authoring.\n *\n * @example\n * ```ts\n * ruleFor('allow', 'viewer', 'posts', ['read'])\n * ruleFor('deny', 'blocked', 'posts', ['read', 'update'])\n * ```\n */\nexport function ruleFor<TAction extends string = string, TData = unknown>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return actions.map((action) => ({\n action,\n effect,\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n resource,\n role,\n ...(options?.when !== undefined ? { when: options.when } : {}),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// allow / deny — ergonomic factories (R12)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action with `effect: 'allow'`.\n *\n * Reads naturally: \"allow editor to read/update posts\".\n *\n * @example\n * ```ts\n * allow('editor', 'posts', ['read', 'update'])\n * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })\n * ```\n */\nexport function allow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('allow', role, resource, actions, options);\n}\n\n/**\n * Creates one `WardRule` per action with `effect: 'deny'`.\n *\n * Reads naturally: \"deny blocked from reading posts\".\n *\n * @example\n * ```ts\n * deny('blocked', 'posts', ['read', 'update'])\n * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })\n * ```\n */\nexport function deny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('deny', role, resource, actions, options);\n}\n\n// ---------------------------------------------------------------------------\n// predicate — grouped predicate helpers (R10)\n// ---------------------------------------------------------------------------\n\n/**\n * Grouped predicate factories. Import as a namespace to avoid name collisions:\n * ```ts\n * import { predicate } from '@vielzeug/ward';\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })\n * ```\n */\nexport const predicate = {\n /**\n * Returns a `WardPredicate` that combines all given predicates with AND semantics —\n * all must return `true` for the rule to match.\n */\n and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.every((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that inverts the given predicate.\n */\n not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData> {\n return (ctx) => !pred(ctx);\n },\n\n /**\n * Returns a `WardPredicate` that combines all given predicates with OR semantics —\n * at least one must return `true` for the rule to match.\n */\n or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.some((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`. Use to express ownership constraints.\n *\n * Must be used with a rule that requires authentication (non-`ANONYMOUS` role).\n * Predicates only execute for authenticated principals — pairing `owns` with an\n * `ANONYMOUS`-role rule produces a rule that can never match because the predicate\n * is skipped for unauthenticated requests.\n *\n * @example\n * ```ts\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * ```\n */\n owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n ): WardPredicate<TData> {\n return ({ data, principal }) => {\n if (!data || typeof data !== 'object') return false;\n\n const record = data as Record<string, unknown>;\n\n if (!Object.hasOwn(record, attributeKey)) return false;\n\n return record[attributeKey] === principal.id;\n };\n },\n} as const;\n\n// ---------------------------------------------------------------------------\n// owns — top-level re-export for backward-compatible usage\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`.\n *\n * Also available as `predicate.owns()` when using the grouped namespace.\n */\nexport function owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n): WardPredicate<TData> {\n return predicate.owns(attributeKey);\n}\n"],"mappings":"AA2BA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAQ,IAAK,IAAY,CAC9B,SACA,SACA,GAAI,GAAS,WAAa,IAAA,GAA6C,CAAC,EAAlC,CAAE,SAAU,EAAQ,QAAS,EACnE,WACA,OACA,GAAI,GAAS,OAAS,IAAA,GAAqC,CAAC,EAA1B,CAAE,KAAM,EAAQ,IAAK,CACzD,EAAE,CACJ,CAiBA,SAAgB,EACd,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAQ,QAAS,EAAM,EAAU,EAAS,CAAO,CAC1D,CAaA,SAAgB,EACd,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAQ,OAAQ,EAAM,EAAU,EAAS,CAAO,CACzD,CAcA,IAAa,EAAY,CAKvB,IAAqB,GAAG,EAAqD,CAC3E,MAAQ,IAAQ,EAAM,MAAO,GAAM,EAAE,CAAG,CAAC,CAC3C,EAKA,IAAqB,EAAkD,CACrE,MAAQ,IAAQ,CAAC,EAAK,CAAG,CAC3B,EAMA,GAAoB,GAAG,EAAqD,CAC1E,MAAQ,IAAQ,EAAM,KAAM,GAAM,EAAE,CAAG,CAAC,CAC1C,EAgBA,KACE,EACsB,CACtB,OAAQ,CAAE,OAAM,eAAgB,CAC9B,GAAI,CAAC,GAAQ,OAAO,GAAS,SAAU,MAAO,GAE9C,IAAM,EAAS,EAIf,OAFK,OAAO,OAAO,EAAQ,CAAY,EAEhC,EAAO,KAAkB,EAAU,GAFO,EAGnD,CACF,CACF,EAYA,SAAgB,EACd,EACsB,CACtB,OAAO,EAAU,KAAK,CAAY,CACpC"}
1
+ {"version":3,"file":"builder.cjs","names":[],"sources":["../src/builder.ts"],"sourcesContent":["import type { WILDCARD } from './constants';\nimport type { WardPredicate, WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// RuleOptions — shared options for allow/deny\n// ---------------------------------------------------------------------------\n\ntype RuleOptions<TData> = {\n priority?: number;\n when?: WardPredicate<TData>;\n};\n\n// ---------------------------------------------------------------------------\n// buildRulesshared implementation\n// ---------------------------------------------------------------------------\n\nfunction buildRules<TAction extends string, TData>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options: RuleOptions<TData> | undefined,\n): WardRule<TAction, TData>[] {\n return actions.map((action) => ({\n action,\n effect,\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n resource,\n role,\n ...(options?.when !== undefined ? { when: options.when } : {}),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// allow / deny — ergonomic factories\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action with `effect: 'allow'`.\n *\n * Reads naturally: \"allow editor to read/update posts\".\n *\n * @example\n * ```ts\n * allow('editor', 'posts', ['read', 'update'])\n * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })\n * ```\n */\nexport function allow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return buildRules('allow', role, resource, actions, options);\n}\n\n/**\n * Creates one `WardRule` per action with `effect: 'deny'`.\n *\n * Reads naturally: \"deny blocked from reading posts\".\n *\n * @example\n * ```ts\n * deny('blocked', 'posts', ['read', 'update'])\n * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })\n * ```\n */\nexport function deny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return buildRules('deny', role, resource, actions, options);\n}\n\n// ---------------------------------------------------------------------------\n// predicate — grouped predicate helpers (R10)\n// ---------------------------------------------------------------------------\n\n/**\n * Grouped predicate factories. Import as a namespace to avoid name collisions:\n * ```ts\n * import { predicate } from '@vielzeug/ward';\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })\n * ```\n */\nexport const predicate = {\n /**\n * Returns a `WardPredicate` that combines all given predicates with AND semantics —\n * all must return `true` for the rule to match.\n */\n and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.every((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that inverts the given predicate.\n */\n not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData> {\n return (ctx) => !pred(ctx);\n },\n\n /**\n * Returns a `WardPredicate` that combines all given predicates with OR semantics —\n * at least one must return `true` for the rule to match.\n */\n or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.some((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`. Use to express ownership constraints.\n *\n * Must be used with a rule that requires authentication (non-`ANONYMOUS` role).\n * Predicates only execute for authenticated principals — pairing `owns` with an\n * `ANONYMOUS`-role rule produces a rule that can never match because the predicate\n * is skipped for unauthenticated requests.\n *\n * @example\n * ```ts\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * ```\n */\n owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n ): WardPredicate<TData> {\n return ({ data, principal }) => {\n if (!data || typeof data !== 'object') return false;\n\n const record = data as Record<string, unknown>;\n\n if (!Object.hasOwn(record, attributeKey)) return false;\n\n return record[attributeKey] === principal.id;\n };\n },\n} as const;\n"],"mappings":"AAgBA,SAAS,EACP,EACA,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAQ,IAAK,IAAY,CAC9B,SACA,SACA,GAAI,GAAS,WAAa,IAAA,GAA6C,CAAC,EAAlC,CAAE,SAAU,EAAQ,QAAS,EACnE,WACA,OACA,GAAI,GAAS,OAAS,IAAA,GAAqC,CAAC,EAA1B,CAAE,KAAM,EAAQ,IAAK,CACzD,EAAE,CACJ,CAiBA,SAAgB,EACd,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAW,QAAS,EAAM,EAAU,EAAS,CAAO,CAC7D,CAaA,SAAgB,EACd,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAW,OAAQ,EAAM,EAAU,EAAS,CAAO,CAC5D,CAcA,IAAa,EAAY,CAKvB,IAAqB,GAAG,EAAqD,CAC3E,MAAQ,IAAQ,EAAM,MAAO,GAAM,EAAE,CAAG,CAAC,CAC3C,EAKA,IAAqB,EAAkD,CACrE,MAAQ,IAAQ,CAAC,EAAK,CAAG,CAC3B,EAMA,GAAoB,GAAG,EAAqD,CAC1E,MAAQ,IAAQ,EAAM,KAAM,GAAM,EAAE,CAAG,CAAC,CAC1C,EAgBA,KACE,EACsB,CACtB,OAAQ,CAAE,OAAM,eAAgB,CAC9B,GAAI,CAAC,GAAQ,OAAO,GAAS,SAAU,MAAO,GAE9C,IAAM,EAAS,EAIf,OAFK,OAAO,OAAO,EAAQ,CAAY,EAEhC,EAAO,KAAkB,EAAU,GAFO,EAGnD,CACF,CACF"}
package/dist/builder.d.ts CHANGED
@@ -4,18 +4,6 @@ type RuleOptions<TData> = {
4
4
  priority?: number;
5
5
  when?: WardPredicate<TData>;
6
6
  };
7
- /**
8
- * Creates one `WardRule` per action for a given effect, role(s), and resource.
9
- *
10
- * Prefer `allow()` or `deny()` for ergonomic rule authoring.
11
- *
12
- * @example
13
- * ```ts
14
- * ruleFor('allow', 'viewer', 'posts', ['read'])
15
- * ruleFor('deny', 'blocked', 'posts', ['read', 'update'])
16
- * ```
17
- */
18
- export declare function ruleFor<TAction extends string = string, TData = unknown>(effect: 'allow' | 'deny', role: string | readonly string[], resource: string | typeof WILDCARD, actions: readonly (TAction | NoInfer<typeof WILDCARD>)[], options?: RuleOptions<TData>): WardRule<TAction, TData>[];
19
7
  /**
20
8
  * Creates one `WardRule` per action with `effect: 'allow'`.
21
9
  *
@@ -79,12 +67,5 @@ export declare const predicate: {
79
67
  */
80
68
  readonly owns: <TData = unknown>(attributeKey: [keyof TData] extends [never] ? string : keyof TData & string) => WardPredicate<TData>;
81
69
  };
82
- /**
83
- * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field
84
- * matches the principal's `id`.
85
- *
86
- * Also available as `predicate.owns()` when using the grouped namespace.
87
- */
88
- export declare function owns<TData = unknown>(attributeKey: [keyof TData] extends [never] ? string : keyof TData & string): WardPredicate<TData>;
89
70
  export {};
90
71
  //# sourceMappingURL=builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAMvD,KAAK,WAAW,CAAC,KAAK,IAAI;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;CAC7B,CAAC;AAMF;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACtE,MAAM,EAAE,OAAO,GAAG,MAAM,EACxB,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,QAAQ,CAAC,CAAC,EAAE,EACxD,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAS5B;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACpE,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,QAAQ,CAAC,CAAC,EAAE,EACxD,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAE5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACnE,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,QAAQ,CAAC,CAAC,EAAE,EACxD,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAE5B;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS;IACpB;;;OAGG;mBACC,KAAK,sBAAsB,aAAa,CAAC,KAAK,CAAC,EAAE,KAAG,aAAa,CAAC,KAAK,CAAC;IAI5E;;OAEG;mBACC,KAAK,kBAAkB,aAAa,CAAC,KAAK,CAAC,KAAG,aAAa,CAAC,KAAK,CAAC;IAItE;;;OAGG;kBACA,KAAK,sBAAsB,aAAa,CAAC,KAAK,CAAC,EAAE,KAAG,aAAa,CAAC,KAAK,CAAC;IAI3E;;;;;;;;;;;;;OAaG;oBACE,KAAK,0BACM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM,KAC1E,aAAa,CAAC,KAAK,CAAC;CAWf,CAAC;AAMX;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,KAAK,GAAG,OAAO,EAClC,YAAY,EAAE,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM,GAC1E,aAAa,CAAC,KAAK,CAAC,CAEtB"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAMvD,KAAK,WAAW,CAAC,KAAK,IAAI;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;CAC7B,CAAC;AA2BF;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACpE,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,QAAQ,CAAC,CAAC,EAAE,EACxD,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAE5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACnE,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,QAAQ,CAAC,CAAC,EAAE,EACxD,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAE5B;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS;IACpB;;;OAGG;mBACC,KAAK,sBAAsB,aAAa,CAAC,KAAK,CAAC,EAAE,KAAG,aAAa,CAAC,KAAK,CAAC;IAI5E;;OAEG;mBACC,KAAK,kBAAkB,aAAa,CAAC,KAAK,CAAC,KAAG,aAAa,CAAC,KAAK,CAAC;IAItE;;;OAGG;kBACA,KAAK,sBAAsB,aAAa,CAAC,KAAK,CAAC,EAAE,KAAG,aAAa,CAAC,KAAK,CAAC;IAI3E;;;;;;;;;;;;;OAaG;oBACE,KAAK,0BACM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM,KAC1E,aAAa,CAAC,KAAK,CAAC;CAWf,CAAC"}
package/dist/builder.js CHANGED
@@ -33,10 +33,7 @@ var r = {
33
33
  };
34
34
  }
35
35
  };
36
- function i(e) {
37
- return r.owns(e);
38
- }
39
36
  //#endregion
40
- export { t as allow, n as deny, i as owns, r as predicate, e as ruleFor };
37
+ export { t as allow, n as deny, r as predicate };
41
38
 
42
39
  //# sourceMappingURL=builder.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"builder.js","names":[],"sources":["../src/builder.ts"],"sourcesContent":["import type { WILDCARD } from './constants';\nimport type { WardPredicate, WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// RuleOptions — shared options for allow/deny/ruleFor\n// ---------------------------------------------------------------------------\n\ntype RuleOptions<TData> = {\n priority?: number;\n when?: WardPredicate<TData>;\n};\n\n// ---------------------------------------------------------------------------\n// ruleFormulti-action rule factory (low-level, effect as first arg)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action for a given effect, role(s), and resource.\n *\n * Prefer `allow()` or `deny()` for ergonomic rule authoring.\n *\n * @example\n * ```ts\n * ruleFor('allow', 'viewer', 'posts', ['read'])\n * ruleFor('deny', 'blocked', 'posts', ['read', 'update'])\n * ```\n */\nexport function ruleFor<TAction extends string = string, TData = unknown>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return actions.map((action) => ({\n action,\n effect,\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n resource,\n role,\n ...(options?.when !== undefined ? { when: options.when } : {}),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// allow / deny — ergonomic factories (R12)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action with `effect: 'allow'`.\n *\n * Reads naturally: \"allow editor to read/update posts\".\n *\n * @example\n * ```ts\n * allow('editor', 'posts', ['read', 'update'])\n * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })\n * ```\n */\nexport function allow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('allow', role, resource, actions, options);\n}\n\n/**\n * Creates one `WardRule` per action with `effect: 'deny'`.\n *\n * Reads naturally: \"deny blocked from reading posts\".\n *\n * @example\n * ```ts\n * deny('blocked', 'posts', ['read', 'update'])\n * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })\n * ```\n */\nexport function deny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('deny', role, resource, actions, options);\n}\n\n// ---------------------------------------------------------------------------\n// predicate — grouped predicate helpers (R10)\n// ---------------------------------------------------------------------------\n\n/**\n * Grouped predicate factories. Import as a namespace to avoid name collisions:\n * ```ts\n * import { predicate } from '@vielzeug/ward';\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })\n * ```\n */\nexport const predicate = {\n /**\n * Returns a `WardPredicate` that combines all given predicates with AND semantics —\n * all must return `true` for the rule to match.\n */\n and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.every((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that inverts the given predicate.\n */\n not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData> {\n return (ctx) => !pred(ctx);\n },\n\n /**\n * Returns a `WardPredicate` that combines all given predicates with OR semantics —\n * at least one must return `true` for the rule to match.\n */\n or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.some((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`. Use to express ownership constraints.\n *\n * Must be used with a rule that requires authentication (non-`ANONYMOUS` role).\n * Predicates only execute for authenticated principals — pairing `owns` with an\n * `ANONYMOUS`-role rule produces a rule that can never match because the predicate\n * is skipped for unauthenticated requests.\n *\n * @example\n * ```ts\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * ```\n */\n owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n ): WardPredicate<TData> {\n return ({ data, principal }) => {\n if (!data || typeof data !== 'object') return false;\n\n const record = data as Record<string, unknown>;\n\n if (!Object.hasOwn(record, attributeKey)) return false;\n\n return record[attributeKey] === principal.id;\n };\n },\n} as const;\n\n// ---------------------------------------------------------------------------\n// owns — top-level re-export for backward-compatible usage\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`.\n *\n * Also available as `predicate.owns()` when using the grouped namespace.\n */\nexport function owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n): WardPredicate<TData> {\n return predicate.owns(attributeKey);\n}\n"],"mappings":";AA2BA,SAAgB,EACd,GACA,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,KAAK,OAAY;EAC9B;EACA;EACA,GAAI,GAAS,aAAa,KAAA,IAA6C,CAAC,IAAlC,EAAE,UAAU,EAAQ,SAAS;EACnE;EACA;EACA,GAAI,GAAS,SAAS,KAAA,IAAqC,CAAC,IAA1B,EAAE,MAAM,EAAQ,KAAK;CACzD,EAAE;AACJ;AAiBA,SAAgB,EACd,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,SAAS,GAAM,GAAU,GAAS,CAAO;AAC1D;AAaA,SAAgB,EACd,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,QAAQ,GAAM,GAAU,GAAS,CAAO;AACzD;AAcA,IAAa,IAAY;CAKvB,IAAqB,GAAG,GAAqD;EAC3E,QAAQ,MAAQ,EAAM,OAAO,MAAM,EAAE,CAAG,CAAC;CAC3C;CAKA,IAAqB,GAAkD;EACrE,QAAQ,MAAQ,CAAC,EAAK,CAAG;CAC3B;CAMA,GAAoB,GAAG,GAAqD;EAC1E,QAAQ,MAAQ,EAAM,MAAM,MAAM,EAAE,CAAG,CAAC;CAC1C;CAgBA,KACE,GACsB;EACtB,QAAQ,EAAE,SAAM,mBAAgB;GAC9B,IAAI,CAAC,KAAQ,OAAO,KAAS,UAAU,OAAO;GAE9C,IAAM,IAAS;GAIf,OAFK,OAAO,OAAO,GAAQ,CAAY,IAEhC,EAAO,OAAkB,EAAU,KAFO;EAGnD;CACF;AACF;AAYA,SAAgB,EACd,GACsB;CACtB,OAAO,EAAU,KAAK,CAAY;AACpC"}
1
+ {"version":3,"file":"builder.js","names":[],"sources":["../src/builder.ts"],"sourcesContent":["import type { WILDCARD } from './constants';\nimport type { WardPredicate, WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// RuleOptions — shared options for allow/deny\n// ---------------------------------------------------------------------------\n\ntype RuleOptions<TData> = {\n priority?: number;\n when?: WardPredicate<TData>;\n};\n\n// ---------------------------------------------------------------------------\n// buildRulesshared implementation\n// ---------------------------------------------------------------------------\n\nfunction buildRules<TAction extends string, TData>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options: RuleOptions<TData> | undefined,\n): WardRule<TAction, TData>[] {\n return actions.map((action) => ({\n action,\n effect,\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n resource,\n role,\n ...(options?.when !== undefined ? { when: options.when } : {}),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// allow / deny — ergonomic factories\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action with `effect: 'allow'`.\n *\n * Reads naturally: \"allow editor to read/update posts\".\n *\n * @example\n * ```ts\n * allow('editor', 'posts', ['read', 'update'])\n * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })\n * ```\n */\nexport function allow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return buildRules('allow', role, resource, actions, options);\n}\n\n/**\n * Creates one `WardRule` per action with `effect: 'deny'`.\n *\n * Reads naturally: \"deny blocked from reading posts\".\n *\n * @example\n * ```ts\n * deny('blocked', 'posts', ['read', 'update'])\n * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })\n * ```\n */\nexport function deny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return buildRules('deny', role, resource, actions, options);\n}\n\n// ---------------------------------------------------------------------------\n// predicate — grouped predicate helpers (R10)\n// ---------------------------------------------------------------------------\n\n/**\n * Grouped predicate factories. Import as a namespace to avoid name collisions:\n * ```ts\n * import { predicate } from '@vielzeug/ward';\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })\n * ```\n */\nexport const predicate = {\n /**\n * Returns a `WardPredicate` that combines all given predicates with AND semantics —\n * all must return `true` for the rule to match.\n */\n and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.every((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that inverts the given predicate.\n */\n not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData> {\n return (ctx) => !pred(ctx);\n },\n\n /**\n * Returns a `WardPredicate` that combines all given predicates with OR semantics —\n * at least one must return `true` for the rule to match.\n */\n or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.some((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`. Use to express ownership constraints.\n *\n * Must be used with a rule that requires authentication (non-`ANONYMOUS` role).\n * Predicates only execute for authenticated principals — pairing `owns` with an\n * `ANONYMOUS`-role rule produces a rule that can never match because the predicate\n * is skipped for unauthenticated requests.\n *\n * @example\n * ```ts\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * ```\n */\n owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n ): WardPredicate<TData> {\n return ({ data, principal }) => {\n if (!data || typeof data !== 'object') return false;\n\n const record = data as Record<string, unknown>;\n\n if (!Object.hasOwn(record, attributeKey)) return false;\n\n return record[attributeKey] === principal.id;\n };\n },\n} as const;\n"],"mappings":";AAgBA,SAAS,EACP,GACA,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,KAAK,OAAY;EAC9B;EACA;EACA,GAAI,GAAS,aAAa,KAAA,IAA6C,CAAC,IAAlC,EAAE,UAAU,EAAQ,SAAS;EACnE;EACA;EACA,GAAI,GAAS,SAAS,KAAA,IAAqC,CAAC,IAA1B,EAAE,MAAM,EAAQ,KAAK;CACzD,EAAE;AACJ;AAiBA,SAAgB,EACd,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAW,SAAS,GAAM,GAAU,GAAS,CAAO;AAC7D;AAaA,SAAgB,EACd,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAW,QAAQ,GAAM,GAAU,GAAS,CAAO;AAC5D;AAcA,IAAa,IAAY;CAKvB,IAAqB,GAAG,GAAqD;EAC3E,QAAQ,MAAQ,EAAM,OAAO,MAAM,EAAE,CAAG,CAAC;CAC3C;CAKA,IAAqB,GAAkD;EACrE,QAAQ,MAAQ,CAAC,EAAK,CAAG;CAC3B;CAMA,GAAoB,GAAG,GAAqD;EAC1E,QAAQ,MAAQ,EAAM,MAAM,MAAM,EAAE,CAAG,CAAC;CAC1C;CAgBA,KACE,GACsB;EACtB,QAAQ,EAAE,SAAM,mBAAgB;GAC9B,IAAI,CAAC,KAAQ,OAAO,KAAS,UAAU,OAAO;GAE9C,IAAM,IAAS;GAIf,OAFK,OAAO,OAAO,GAAQ,CAAY,IAEhC,EAAO,OAAkB,EAAU,KAFO;EAGnD;CACF;AACF"}
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");var i=require("./_dev.cjs").warnAnonymousPredicates;function a(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 o(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 s(s=[],c={}){if(c.maxConflicts!==void 0&&(!Number.isFinite(c.maxConflicts)||c.maxConflicts<0))throw new e.WardConfigError(`maxConflicts must be a finite non-negative number.`);if(c.onConflict!==void 0&&typeof c.onConflict!=`function`)throw new e.WardConfigError(`onConflict must be a function.`);let{maxConflicts:l=1/0}=c,u=new Set,d=[];for(let e of s)if(Array.isArray(e))for(let t of e)d.push(t);else d.push(e);let f=d.map((e,n)=>t.compileEntry(e,n));i(f);function p(e){if(u.size!==0)for(let t of u)try{t(e)}catch{}}function m(e,t,r,i){let a=n.pickWinner(f,e,t,r,i),o=n.toDecision(a);return p({action:r,data:i,decision:o,principal:e,resource:t,type:`decision`}),o}function h(e){let{action:t,data:r,principal:i,resource:a}=e;return n.validatePrincipal(i),m(i,a,t,r)}function g(e,t){return t.map(t=>({...m(e,t.resource,t.action,t.data),action:t.action,resource:t.resource}))}function _(e,t){return t.length===0?[]:(n.validatePrincipal(e),g(e,t))}function v(e){let{data:t,knownActions:r,principal:i,resource:o}=e;return n.validatePrincipal(i),a(f,i,o,r,t)}function y(e){let{data:t,principal:r,resource:i}=e;return n.validatePrincipal(r),o(f,r,i,t)}function b(e){let{action:t,data:r,principal:i,resource:a}=e;n.validatePrincipal(i);let o=[];for(let e of f)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 x(e){n.assertUserPrincipal(e);let t={attributes:e.attributes?structuredClone(e.attributes):void 0,id:e.id,roles:[...e.roles]};return{allowedActions:e=>a(f,t,e.resource,e.knownActions,e.data),checkAll:e=>e.length===0?[]:g(t,e),explain:e=>m(t,e.resource,e.action,e.data),rulesInScope:e=>o(f,t,e.resource,e.data),trace:e=>b({action:e.action,data:e.data,principal:t,resource:e.resource})}}let S;function C(){return S??=Object.freeze(r.computeConflicts(f,l))}if(c.strict||c.onConflict){let t=C();if(t.length>0&&(c.onConflict&&t.forEach(c.onConflict),c.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}`)}}function w(e,t){u.add(e);let n=()=>u.delete(e);if(t?.signal){if(t.signal.aborted)return u.delete(e),()=>{};t.signal.addEventListener(`abort`,n,{once:!0})}return()=>{u.delete(e),t?.signal?.removeEventListener(`abort`,n)}}return{allowedActions:v,checkAll:_,detectConflicts:C,explain:h,forUser:x,rulesInScope:y,tap:w,trace:b}}exports.createWard=s;
1
+ const e=require("./errors.cjs"),t=require("./_compile.cjs"),n=require("./_match.cjs"),r=require("./_conflict.cjs");var i=require("./_dev.cjs").warnAnonymousPredicates;function a(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 o(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 s(s=[],c={}){if(c.maxConflicts!==void 0&&(!Number.isFinite(c.maxConflicts)||c.maxConflicts<0))throw new e.WardConfigError(`maxConflicts must be a finite non-negative number.`);if(c.onConflict!==void 0&&typeof c.onConflict!=`function`)throw new e.WardConfigError(`onConflict must be a function.`);let{maxConflicts:l=1/0}=c,u=new Set,d=[];for(let e of s)if(Array.isArray(e))for(let t of e)d.push(t);else d.push(e);let f=d.map((e,n)=>t.compileEntry(e,n));i(f);function p(e){if(u.size!==0)for(let t of u)try{t(e)}catch{}}function m(e,t,r,i){let a=n.pickWinner(f,e,t,r,i),o=n.toDecision(a);return p({action:r,data:i,decision:o,principal:e,resource:t}),o}function h(e){let{action:t,data:r,principal:i,resource:a}=e;return n.validatePrincipal(i),m(i,a,t,r)}function g(e,t){return t.map(t=>({...m(e,t.resource,t.action,t.data),action:t.action,resource:t.resource}))}function _(e,t){return t.length===0?[]:(n.validatePrincipal(e),g(e,t))}function v(e){let{data:t,knownActions:r,principal:i,resource:o}=e;return n.validatePrincipal(i),a(f,i,o,r,t)}function y(e){let{data:t,principal:r,resource:i}=e;return n.validatePrincipal(r),o(f,r,i,t)}function b(e){let{action:t,data:r,principal:i,resource:a}=e;n.validatePrincipal(i);let o=[];for(let e of f)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 x(e){n.assertUserPrincipal(e);let t={attributes:e.attributes?structuredClone(e.attributes):void 0,id:e.id,roles:[...e.roles]};return{allowedActions:e=>a(f,t,e.resource,e.knownActions,e.data),checkAll:e=>e.length===0?[]:g(t,e),explain:e=>m(t,e.resource,e.action,e.data),rulesInScope:e=>o(f,t,e.resource,e.data),trace:e=>b({action:e.action,data:e.data,principal:t,resource:e.resource})}}let S;function C(){return S??=Object.freeze(r.computeConflicts(f,l))}if(c.strict||c.onConflict){let t=C();if(t.length>0&&(c.onConflict&&t.forEach(c.onConflict),c.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}`)}}function w(e,t){u.add(e);let n=()=>u.delete(e);if(t?.signal){if(t.signal.aborted)return u.delete(e),()=>{};t.signal.addEventListener(`abort`,n,{once:!0})}return()=>{u.delete(e),t?.signal?.removeEventListener(`abort`,n)}}return{allowedActions:v,checkAll:_,detectConflicts:C,explain:h,forUser:x,rulesInScope:y,tap:w,trace:b}}exports.createWard=s;
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 { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { warnAnonymousPredicates } from './_dev';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\nimport type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n NormalizedWardRule,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nconst warn = warnAnonymousPredicates;\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): NormalizedWardRule<TAction, TData>[] {\n const skipPredicate = data === undefined;\n const result: NormalizedWardRule<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule as NormalizedWardRule<TAction, TData>);\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> | readonly WardRule<TAction, TData>[])[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n if (options.maxConflicts !== undefined) {\n if (!Number.isFinite(options.maxConflicts) || options.maxConflicts < 0) {\n throw new WardConfigError('maxConflicts must be a finite non-negative number.');\n }\n }\n\n if (options.onConflict !== undefined && typeof options.onConflict !== 'function') {\n throw new WardConfigError('onConflict must be a function.');\n }\n\n const { maxConflicts = Infinity } = options;\n const tappers = new Set<(event: WardEvent<TAction, TData>) => void>();\n const flat: WardRule<TAction, TData>[] = [];\n\n for (const entry of rules) {\n if (Array.isArray(entry)) {\n for (const rule of entry) flat.push(rule);\n } else {\n flat.push(entry as WardRule<TAction, TData>);\n }\n }\n\n const entries = flat.map((rule, i) => compileEntry(rule, i));\n\n // Warn in development when an ANONYMOUS-role rule has a predicate.\n warn(entries);\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function emitTap(event: WardEvent<TAction, TData>): void {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must not affect ward behavior.\n }\n }\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 emitTap({ action, data, decision, principal, resource, type: '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(\n input: WardRulesInScopeInput<TData>,\n ): ReadonlyArray<Readonly<NormalizedWardRule<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 function tap(handler: (event: WardEvent<TAction, TData>) => void, opts?: { signal?: AbortSignal }): () => void {\n tappers.add(handler);\n\n const onAbort = () => tappers.delete(handler);\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n tappers.delete(handler);\n return () => {};\n }\n opts.signal.addEventListener('abort', onAbort, { once: true });\n }\n\n return () => {\n tappers.delete(handler);\n opts?.signal?.removeEventListener('abort', onAbort);\n };\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, tap, 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 { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { warnAnonymousPredicates } from './_dev';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\nimport type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n NormalizedWardRule,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nconst warn = warnAnonymousPredicates;\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): NormalizedWardRule<TAction, TData>[] {\n const skipPredicate = data === undefined;\n const result: NormalizedWardRule<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule as NormalizedWardRule<TAction, TData>);\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> | readonly WardRule<TAction, TData>[])[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n if (options.maxConflicts !== undefined) {\n if (!Number.isFinite(options.maxConflicts) || options.maxConflicts < 0) {\n throw new WardConfigError('maxConflicts must be a finite non-negative number.');\n }\n }\n\n if (options.onConflict !== undefined && typeof options.onConflict !== 'function') {\n throw new WardConfigError('onConflict must be a function.');\n }\n\n const { maxConflicts = Infinity } = options;\n const tappers = new Set<(event: WardEvent<TAction, TData>) => void>();\n const flat: WardRule<TAction, TData>[] = [];\n\n for (const entry of rules) {\n if (Array.isArray(entry)) {\n for (const rule of entry) flat.push(rule);\n } else {\n flat.push(entry as WardRule<TAction, TData>);\n }\n }\n\n const entries = flat.map((rule, i) => compileEntry(rule, i));\n\n // Warn in development when an ANONYMOUS-role rule has a predicate.\n warn(entries);\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function emitTap(event: WardEvent<TAction, TData>): void {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must not affect ward behavior.\n }\n }\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 emitTap({ action, data, decision, principal, resource });\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(\n input: WardRulesInScopeInput<TData>,\n ): ReadonlyArray<Readonly<NormalizedWardRule<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 function tap(handler: (event: WardEvent<TAction, TData>) => void, opts?: { signal?: AbortSignal }): () => void {\n tappers.add(handler);\n\n const onAbort = () => tappers.delete(handler);\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n tappers.delete(handler);\n return () => {};\n }\n opts.signal.addEventListener('abort', onAbort, { once: true });\n }\n\n return () => {\n tappers.delete(handler);\n opts?.signal?.removeEventListener('abort', onAbort);\n };\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, tap, trace };\n}\n"],"mappings":"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"}
package/dist/factory.js CHANGED
@@ -35,8 +35,7 @@ function p(l = [], p = {}) {
35
35
  data: r,
36
36
  decision: s,
37
37
  principal: e,
38
- resource: t,
39
- type: "decision"
38
+ resource: t
40
39
  }), s;
41
40
  }
42
41
  function b(e) {
@@ -1 +1 @@
1
- {"version":3,"file":"factory.js","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { warnAnonymousPredicates } from './_dev';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\nimport type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n NormalizedWardRule,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nconst warn = warnAnonymousPredicates;\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): NormalizedWardRule<TAction, TData>[] {\n const skipPredicate = data === undefined;\n const result: NormalizedWardRule<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule as NormalizedWardRule<TAction, TData>);\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> | readonly WardRule<TAction, TData>[])[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n if (options.maxConflicts !== undefined) {\n if (!Number.isFinite(options.maxConflicts) || options.maxConflicts < 0) {\n throw new WardConfigError('maxConflicts must be a finite non-negative number.');\n }\n }\n\n if (options.onConflict !== undefined && typeof options.onConflict !== 'function') {\n throw new WardConfigError('onConflict must be a function.');\n }\n\n const { maxConflicts = Infinity } = options;\n const tappers = new Set<(event: WardEvent<TAction, TData>) => void>();\n const flat: WardRule<TAction, TData>[] = [];\n\n for (const entry of rules) {\n if (Array.isArray(entry)) {\n for (const rule of entry) flat.push(rule);\n } else {\n flat.push(entry as WardRule<TAction, TData>);\n }\n }\n\n const entries = flat.map((rule, i) => compileEntry(rule, i));\n\n // Warn in development when an ANONYMOUS-role rule has a predicate.\n warn(entries);\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function emitTap(event: WardEvent<TAction, TData>): void {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must not affect ward behavior.\n }\n }\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 emitTap({ action, data, decision, principal, resource, type: '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(\n input: WardRulesInScopeInput<TData>,\n ): ReadonlyArray<Readonly<NormalizedWardRule<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 function tap(handler: (event: WardEvent<TAction, TData>) => void, opts?: { signal?: AbortSignal }): () => void {\n tappers.add(handler);\n\n const onAbort = () => tappers.delete(handler);\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n tappers.delete(handler);\n return () => {};\n }\n opts.signal.addEventListener('abort', onAbort, { once: true });\n }\n\n return () => {\n tappers.delete(handler);\n opts?.signal?.removeEventListener('abort', onAbort);\n };\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, tap, 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 { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { warnAnonymousPredicates } from './_dev';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\nimport type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n NormalizedWardRule,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nconst warn = warnAnonymousPredicates;\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): NormalizedWardRule<TAction, TData>[] {\n const skipPredicate = data === undefined;\n const result: NormalizedWardRule<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule as NormalizedWardRule<TAction, TData>);\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> | readonly WardRule<TAction, TData>[])[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n if (options.maxConflicts !== undefined) {\n if (!Number.isFinite(options.maxConflicts) || options.maxConflicts < 0) {\n throw new WardConfigError('maxConflicts must be a finite non-negative number.');\n }\n }\n\n if (options.onConflict !== undefined && typeof options.onConflict !== 'function') {\n throw new WardConfigError('onConflict must be a function.');\n }\n\n const { maxConflicts = Infinity } = options;\n const tappers = new Set<(event: WardEvent<TAction, TData>) => void>();\n const flat: WardRule<TAction, TData>[] = [];\n\n for (const entry of rules) {\n if (Array.isArray(entry)) {\n for (const rule of entry) flat.push(rule);\n } else {\n flat.push(entry as WardRule<TAction, TData>);\n }\n }\n\n const entries = flat.map((rule, i) => compileEntry(rule, i));\n\n // Warn in development when an ANONYMOUS-role rule has a predicate.\n warn(entries);\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function emitTap(event: WardEvent<TAction, TData>): void {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must not affect ward behavior.\n }\n }\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 emitTap({ action, data, decision, principal, resource });\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(\n input: WardRulesInScopeInput<TData>,\n ): ReadonlyArray<Readonly<NormalizedWardRule<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 function tap(handler: (event: WardEvent<TAction, TData>) => void, opts?: { signal?: AbortSignal }): () => void {\n tappers.add(handler);\n\n const onAbort = () => tappers.delete(handler);\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n tappers.delete(handler);\n return () => {};\n }\n opts.signal.addEventListener('abort', onAbort, { once: true });\n }\n\n return () => {\n tappers.delete(handler);\n opts?.signal?.removeEventListener('abort', onAbort);\n };\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, tap, trace };\n}\n"],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./builder.cjs"),t=require("./constants.cjs"),n=require("./errors.cjs"),r=require("./resource.cjs"),i=require("./factory.cjs");exports.ANONYMOUS=t.ANONYMOUS,exports.WILDCARD=t.WILDCARD,exports.WardConfigError=n.WardConfigError,exports.WardError=n.WardError,exports.WardPredicateError=n.WardPredicateError,exports.allow=e.allow,exports.createWard=i.createWard,exports.deny=e.deny,exports.matchesPattern=r.matchesPattern,exports.owns=e.owns,exports.patternCovers=r.patternCovers,exports.predicate=e.predicate,exports.ruleFor=e.ruleFor;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./builder.cjs"),t=require("./constants.cjs"),n=require("./errors.cjs"),r=require("./resource.cjs"),i=require("./factory.cjs");exports.ANONYMOUS=t.ANONYMOUS,exports.WILDCARD=t.WILDCARD,exports.WardConfigError=n.WardConfigError,exports.WardError=n.WardError,exports.WardPredicateError=n.WardPredicateError,exports.allow=e.allow,exports.createWard=i.createWard,exports.deny=e.deny,exports.matchesPattern=r.matchesPattern,exports.patternCovers=r.patternCovers,exports.predicate=e.predicate;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { allow, deny, owns, predicate, ruleFor } from './builder';
1
+ export { allow, deny, predicate } from './builder';
2
2
  export { ANONYMOUS, WILDCARD } from './constants';
3
3
  export { WardConfigError, WardError, WardPredicateError } from './errors';
4
4
  export { createWard } from './factory';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3D,YAAY,EACV,SAAS,EACT,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,EAC1B,YAAY,EACZ,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,aAAa,EACb,IAAI,EACJ,uBAAuB,EACvB,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,aAAa,EACb,QAAQ,EACR,qBAAqB,EACrB,SAAS,EACT,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3D,YAAY,EACV,SAAS,EACT,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,EAC1B,YAAY,EACZ,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,aAAa,EACb,IAAI,EACJ,uBAAuB,EACvB,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,aAAa,EACb,QAAQ,EACR,qBAAqB,EACrB,SAAS,EACT,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { allow as e, deny as t, owns as n, predicate as r, ruleFor as i } from "./builder.js";
2
- import { ANONYMOUS as a, WILDCARD as o } from "./constants.js";
3
- import { WardConfigError as s, WardError as c, WardPredicateError as l } from "./errors.js";
4
- import { matchesPattern as u, patternCovers as d } from "./resource.js";
5
- import { createWard as f } from "./factory.js";
6
- export { a as ANONYMOUS, o as WILDCARD, s as WardConfigError, c as WardError, l as WardPredicateError, e as allow, f as createWard, t as deny, u as matchesPattern, n as owns, d as patternCovers, r as predicate, i as ruleFor };
1
+ import { allow as e, deny as t, predicate as n } from "./builder.js";
2
+ import { ANONYMOUS as r, WILDCARD as i } from "./constants.js";
3
+ import { WardConfigError as a, WardError as o, WardPredicateError as s } from "./errors.js";
4
+ import { matchesPattern as c, patternCovers as l } from "./resource.js";
5
+ import { createWard as u } from "./factory.js";
6
+ export { r as ANONYMOUS, i as WILDCARD, a as WardConfigError, o as WardError, s as WardPredicateError, e as allow, u as createWard, t as deny, c as matchesPattern, l as patternCovers, n as predicate };
package/dist/types.d.ts CHANGED
@@ -130,7 +130,6 @@ export type BoundWard<TAction extends string = string, TData = unknown> = {
130
130
  * Subscribe via `ward.tap(handler)` — handler errors are swallowed.
131
131
  */
132
132
  export type WardEvent<TAction extends string = string, TData = unknown> = {
133
- readonly type: 'decision';
134
133
  readonly decision: WardDecision<TAction, TData>;
135
134
  readonly action: TAction;
136
135
  readonly data?: TData;