@vielzeug/ward 2.2.0 → 3.0.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.
Files changed (62) hide show
  1. package/README.md +32 -37
  2. package/dist/_compile.cjs +1 -1
  3. package/dist/_compile.cjs.map +1 -1
  4. package/dist/_compile.d.ts +5 -33
  5. package/dist/_compile.d.ts.map +1 -1
  6. package/dist/_compile.js +40 -35
  7. package/dist/_compile.js.map +1 -1
  8. package/dist/_match.cjs +1 -1
  9. package/dist/_match.cjs.map +1 -1
  10. package/dist/_match.d.ts +9 -19
  11. package/dist/_match.d.ts.map +1 -1
  12. package/dist/_match.js +40 -37
  13. package/dist/_match.js.map +1 -1
  14. package/dist/errors.cjs +1 -1
  15. package/dist/errors.cjs.map +1 -1
  16. package/dist/errors.d.ts +3 -16
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/errors.js +2 -3
  19. package/dist/errors.js.map +1 -1
  20. package/dist/factory.cjs +1 -1
  21. package/dist/factory.cjs.map +1 -1
  22. package/dist/factory.d.ts +14 -10
  23. package/dist/factory.d.ts.map +1 -1
  24. package/dist/factory.js +70 -119
  25. package/dist/factory.js.map +1 -1
  26. package/dist/index.cjs +1 -1
  27. package/dist/index.d.ts +3 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +6 -6
  30. package/dist/rules.cjs +2 -0
  31. package/dist/rules.cjs.map +1 -0
  32. package/dist/rules.d.ts +15 -0
  33. package/dist/rules.d.ts.map +1 -0
  34. package/dist/rules.js +26 -0
  35. package/dist/rules.js.map +1 -0
  36. package/dist/types.d.ts +63 -133
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/ward.cjs +1 -1
  39. package/dist/ward.cjs.map +1 -1
  40. package/dist/ward.iife.js +1 -1
  41. package/dist/ward.iife.js.map +1 -1
  42. package/dist/ward.js +1 -1
  43. package/dist/ward.js.map +1 -1
  44. package/package.json +7 -7
  45. package/dist/_conflict.cjs +0 -2
  46. package/dist/_conflict.cjs.map +0 -1
  47. package/dist/_conflict.d.ts +0 -10
  48. package/dist/_conflict.d.ts.map +0 -1
  49. package/dist/_conflict.js +0 -51
  50. package/dist/_conflict.js.map +0 -1
  51. package/dist/_dev.cjs +0 -2
  52. package/dist/_dev.cjs.map +0 -1
  53. package/dist/_dev.d.ts +0 -8
  54. package/dist/_dev.d.ts.map +0 -1
  55. package/dist/_dev.js +0 -6
  56. package/dist/_dev.js.map +0 -1
  57. package/dist/builder.cjs +0 -2
  58. package/dist/builder.cjs.map +0 -1
  59. package/dist/builder.d.ts +0 -90
  60. package/dist/builder.d.ts.map +0 -1
  61. package/dist/builder.js +0 -42
  62. package/dist/builder.js.map +0 -1
package/README.md CHANGED
@@ -1,59 +1,54 @@
1
1
  # @vielzeug/ward
2
2
 
3
- Minimal authorization engine with deterministic precedence, wildcard support, and runtime predicates.
3
+ > Ordered authorization rules with immutable policies and typed decisions
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```sh
8
8
  pnpm add @vielzeug/ward
9
+ npm install @vielzeug/ward
10
+ yarn add @vielzeug/ward
9
11
  ```
10
12
 
11
13
  ## Quick Start
12
14
 
13
15
  ```ts
14
- import { ANONYMOUS, WILDCARD, allow, createWard, deny, owns } from '@vielzeug/ward';
15
-
16
- const ward = createWard<'read' | 'update', { authorId: string }>([
17
- allow([ANONYMOUS, 'viewer'], 'posts', ['read']),
18
- allow('editor', 'posts', ['update'], { when: owns('authorId') }),
19
- deny('blocked', WILDCARD, [WILDCARD], { priority: 100 }),
16
+ import { ANONYMOUS, WILDCARD, allow, createWard, deny, predicate } from '@vielzeug/ward';
17
+
18
+ type Action = 'read' | 'update';
19
+ type Resource = 'posts';
20
+ type Attributes = { authorId: string };
21
+
22
+ const ward = createWard<Action, Resource, Attributes>([
23
+ deny('blocked', WILDCARD, [WILDCARD]),
24
+ allow(ANONYMOUS, 'posts', ['read']),
25
+ allow('viewer', 'posts', ['read']),
26
+ allow<Action, Resource, Attributes>('editor', 'posts', ['update'], {
27
+ when: predicate.owns<Attributes>('authorId'),
28
+ }),
20
29
  ]);
21
30
 
22
31
  const principal = { id: 'u1', roles: ['editor'] };
23
-
24
- const decision = ward.explain({
25
- principal,
26
- resource: 'posts',
32
+ const decision = ward.decide({
27
33
  action: 'update',
28
- data: { authorId: 'u2' },
29
- });
30
-
31
- const batch = ward.checkAll(principal, [
32
- { resource: 'posts', action: 'read' },
33
- { resource: 'posts', action: 'update', data: { authorId: 'u1' } },
34
- ]);
35
-
36
- const trace = ward.trace({
34
+ attributes: { authorId: 'u1' },
37
35
  principal,
38
36
  resource: 'posts',
39
- action: 'update',
40
- data: { authorId: 'u2' },
41
37
  });
42
38
 
43
- const bound = ward.forUser(principal);
44
- bound.allowedActions({ resource: 'posts', knownActions: ['read', 'update', 'delete'] as const });
45
- bound.explain({ resource: 'posts', action: 'update', data: { authorId: 'u2' } });
39
+ if (decision.effect === 'allow') {
40
+ console.log('update permitted');
41
+ }
46
42
  ```
47
43
 
48
- ## API Notes
49
-
50
- 1. `explain()` and `trace()` take object inputs: `{ principal, resource, action, data? }`.
51
- 2. `allowedActions()` takes `{ principal, resource, knownActions, data? }`.
52
- 3. `rulesInScope()` takes `{ principal, resource, data? }`.
53
- 4. `BoundWard` methods use object inputs without `principal`.
54
- 5. `trace()` does not fire a `decision` event; `explain()` and `checkAll()` do.
55
- 6. Use `explain()` directly at request boundaries instead of middleware wrappers.
56
- 7. Subscribe to decision events with `tap()` for logging and diagnostics:
57
- ```ts
58
- ward.tap((event) => console.debug(`ward:${event.type}`, event.decision));
59
- ```
44
+ ## Documentation
45
+
46
+ - [Overview](https://vielzeug.dev/ward/)
47
+ - [Usage Guide](https://vielzeug.dev/ward/usage)
48
+ - [API Reference](https://vielzeug.dev/ward/api)
49
+ - [Examples](https://vielzeug.dev/ward/examples)
50
+ - [Migration Guide](https://vielzeug.dev/ward/migration)
51
+
52
+ ## License
53
+
54
+ MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
package/dist/_compile.cjs CHANGED
@@ -1,2 +1,2 @@
1
- require("./constants.cjs");const e=require("./errors.cjs");function t(t,n){let r=`Rule[${n}]`,i=Array.isArray(t.role)?t.role:[t.role];if(i.length===0||i.some(e=>typeof e!=`string`||!e.trim()))throw new e.WardConfigError(`${r}.role must be a non-empty string or non-empty array of strings`);if(typeof t.resource!=`string`||!t.resource.trim())throw new e.WardConfigError(`${r}.resource must be a non-empty string`);if(t.resource.endsWith(`:`))throw new e.WardConfigError(`${r}.resource '${String(t.resource)}' ends with ':' — did you mean '${String(t.resource)}*'?`);if(typeof t.action!=`string`||!t.action.trim())throw new e.WardConfigError(`${r}.action must be a non-empty string`);if(t.action.endsWith(`:`))throw new e.WardConfigError(`${r}.action '${String(t.action)}' ends with ':' — did you mean '${String(t.action)}*'?`);if(t.effect!==`allow`&&t.effect!==`deny`)throw new e.WardConfigError(`${r}.effect must be "allow" or "deny"`);if(t.priority!==void 0&&(typeof t.priority!=`number`||!Number.isFinite(t.priority)))throw new e.WardConfigError(`${r}.priority must be a finite number`);if(t.when!==void 0&&typeof t.when!=`function`)throw new e.WardConfigError(`${r}.when must be a function`)}function n(e){return e===`*`?0:e.endsWith(`:*`)?1:2}function r(e){return+!e.role.includes(`*`)+n(e.resource)+n(e.action)}function i(e,n){t(e,n);let i=Array.isArray(e.role)?[...e.role]:[e.role],a=Object.freeze([...new Set(i)]),o=Object.freeze({action:e.action,effect:e.effect,priority:e.priority??0,resource:e.resource,role:a,...e.when===void 0?{}:{when:e.when}}),s=r(o),c=o.priority;return{denyBonus:+(o.effect===`deny`),index:n,priority:c,roles:a,rule:o,score:s}}exports.compileEntry=i,exports.patternScore=n,exports.validateRuleInput=t;
1
+ const e=require("./errors.cjs");function t(n,r,i){if(n===null||typeof n==`boolean`||typeof n==`string`)return n;if(typeof n==`number`){if(!Number.isFinite(n))throw new e.WardConfigError(`${r} must contain only finite numbers`);return n}if(typeof n!=`object`)throw new e.WardConfigError(`${r} must contain only JSON-compatible values`);if(i.has(n))throw new e.WardConfigError(`${r} must not contain circular references`);let a=Object.getPrototypeOf(n);if(!Array.isArray(n)&&a!==Object.prototype&&a!==null)throw new e.WardConfigError(`${r} must contain only arrays and plain objects`);i.add(n);try{return Object.freeze(Array.isArray(n)?Array.from(n,(e,n)=>t(e,`${r}[${n}]`,i)):Object.fromEntries(Object.keys(n).map(e=>[e,t(n[e],`${r}.${e}`,i)])))}finally{i.delete(n)}}function n(n,r){if(n!==void 0){if(typeof n!=`object`||!n||Array.isArray(n))throw new e.WardConfigError(`${r} must be a plain object`);return t(n,r,new Set)}}function r(t,r){let i=`Rule[${r}]`;if(typeof t!=`object`||!t||Array.isArray(t))throw new e.WardConfigError(`${i} must be a plain object`);let a=Object.getPrototypeOf(t);if(a!==Object.prototype&&a!==null)throw new e.WardConfigError(`${i} must be a plain object`);if(typeof t.action!=`string`||!t.action.trim())throw new e.WardConfigError(`${i}.action must be a non-empty string`);if(t.action.endsWith(`:`))throw new e.WardConfigError(`${i}.action '${t.action}' ends with ':' — did you mean '${t.action}*'?`);if(typeof t.resource!=`string`||!t.resource.trim())throw new e.WardConfigError(`${i}.resource must be a non-empty string`);if(t.resource.endsWith(`:`))throw new e.WardConfigError(`${i}.resource '${t.resource}' ends with ':' — did you mean '${t.resource}*'?`);if(t.effect!==`allow`&&t.effect!==`deny`)throw new e.WardConfigError(`${i}.effect must be "allow" or "deny"`);if(t.condition!==void 0&&typeof t.condition!=`function`)throw new e.WardConfigError(`${i}.condition must be a function`);return Object.freeze({action:t.action,...t.attributes===void 0?{}:{attributes:n(t.attributes,`${i}.attributes`)},...t.condition===void 0?{}:{condition:t.condition},effect:t.effect,resource:t.resource})}exports.compileRule=r,exports.snapshotAttributes=n;
2
2
  //# sourceMappingURL=_compile.cjs.map
@@ -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 { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':' — did you mean '${rule.action}*'?`);\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.endsWith(':')) {\n throw new WardConfigError(`${at}.resource '${rule.resource}' ends with ':' — did you mean '${rule.resource}*'?`);\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.condition !== undefined && typeof rule.condition !== 'function') {\n throw new WardConfigError(`${at}.condition must be a function`);\n }\n\n return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n"],"mappings":"gCAGA,SAAS,EAAc,EAAgB,EAAc,EAA4C,CAC/F,GAAI,IAAU,MAAQ,OAAO,GAAU,WAAa,OAAO,GAAU,SAAU,OAAO,EAEtF,GAAI,OAAO,GAAU,SAAU,CAC7B,GAAI,CAAC,OAAO,SAAS,CAAK,EAAG,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAK,kCAAkC,EACjG,OAAO,CACT,CAEA,GAAI,OAAO,GAAU,SACnB,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAK,0CAA0C,EAG9E,GAAI,EAAU,IAAI,CAAK,EAAG,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAK,sCAAsC,EAElG,IAAM,EAAY,OAAO,eAAe,CAAK,EAC7C,GAAI,CAAC,MAAM,QAAQ,CAAK,GAAK,IAAc,OAAO,WAAa,IAAc,KAC3E,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAK,4CAA4C,EAGhF,EAAU,IAAI,CAAK,EAEnB,GAAI,CAKF,OAHS,OAAO,OADZ,MAAM,QAAQ,CAAK,EACA,MAAM,KAAK,GAAQ,EAAM,IAAU,EAAc,EAAM,GAAG,EAAK,GAAG,EAAM,GAAI,CAAS,CAAC,EAI3G,OAAO,YACL,OAAO,KAAK,CAAK,CAAC,CAAC,IAAK,GAAQ,CAC9B,EACA,EAAe,EAAkC,GAAM,GAAG,EAAK,GAAG,IAAO,CAAS,CACpF,CAAC,CACH,CAT4G,CAWhH,QAAU,CACR,EAAU,OAAO,CAAK,CACxB,CACF,CAEA,SAAgB,EAAmB,EAAgB,EAA0C,CACvF,OAAU,IAAA,GACd,IAAI,OAAO,GAAU,WAAY,GAAkB,MAAM,QAAQ,CAAK,EACpE,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAK,wBAAwB,EAG5D,OAAO,EAAc,EAAO,EAAM,IAAI,GAAK,CAHiB,CAI9D,CAMA,SAAgB,EACd,EACA,EAC2C,CAC3C,IAAM,EAAK,QAAQ,EAAM,GAEzB,GAAI,OAAO,GAAS,WAAY,GAAiB,MAAM,QAAQ,CAAI,EACjE,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,wBAAwB,EAE1D,IAAM,EAAY,OAAO,eAAe,CAAI,EAC5C,GAAI,IAAc,OAAO,WAAa,IAAc,KAClD,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,wBAAwB,EAG1D,GAAI,OAAO,EAAK,QAAW,UAAY,CAAC,EAAK,OAAO,KAAK,EACvD,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,mCAAmC,EAGrE,GAAI,EAAK,OAAO,SAAS,GAAG,EAC1B,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,WAAW,EAAK,OAAO,kCAAkC,EAAK,OAAO,IAAI,EAG3G,GAAI,OAAO,EAAK,UAAa,UAAY,CAAC,EAAK,SAAS,KAAK,EAC3D,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,qCAAqC,EAGvE,GAAI,EAAK,SAAS,SAAS,GAAG,EAC5B,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,aAAa,EAAK,SAAS,kCAAkC,EAAK,SAAS,IAAI,EAGjH,GAAI,EAAK,SAAW,SAAW,EAAK,SAAW,OAC7C,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,kCAAkC,EAGpE,GAAI,EAAK,YAAc,IAAA,IAAa,OAAO,EAAK,WAAc,WAC5D,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAG,8BAA8B,EAGhE,OAAO,OAAO,OAAO,CACnB,OAAQ,EAAK,OACb,GAAI,EAAK,aAAe,IAAA,GAAY,CAAC,EAAI,CAAE,WAAY,EAAmB,EAAK,WAAY,GAAG,EAAG,YAAY,CAAE,EAC/G,GAAI,EAAK,YAAc,IAAA,GAAY,CAAC,EAAI,CAAE,UAAW,EAAK,SAAU,EACpE,OAAQ,EAAK,OACb,SAAU,EAAK,QACjB,CAAC,CACH"}
@@ -1,36 +1,8 @@
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
- }>;
12
- /** A compiled entry stores the normalized rule plus pre-computed lookup values. */
13
- export type CompiledEntry<TAction extends string, TData> = {
14
- /** Deny bonus (1 for deny, 0 for allow): tiebreaker when priority and score match. */
15
- denyBonus: 0 | 1;
16
- /** Original rule index, used in predicate error messages. */
17
- index: number;
18
- /** Resolved priority (defaults to 0 when not authored). */
19
- priority: number;
20
- /** Normalized roles array (always an array). */
21
- roles: readonly string[];
22
- /** The normalized, frozen compiled rule. */
23
- rule: CompiledRule<TAction, TData>;
24
- /** Specificity score (0–5): roleScore(0|1) + resourceScore(0|1|2) + actionScore(0|1|2). */
25
- score: number;
26
- };
27
- export declare function validateRuleInput<TAction extends string, TData>(rule: WardRule<TAction, TData>, index: number): void;
1
+ import type { WardAttributes, WardRule } from './types';
2
+ export declare function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined;
28
3
  /**
29
- * Returns the specificity score for a single pattern field:
30
- * - 0: global wildcard (`*`)
31
- * - 1: namespace wildcard (e.g. `posts:*` or `read:*`)
32
- * - 2: exact string
4
+ * Validates and snapshots a single rule definition at ward-creation time.
5
+ * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.
33
6
  */
34
- export declare function patternScore(pattern: string): number;
35
- export declare function compileEntry<TAction extends string, TData>(input: WardRule<TAction, TData>, index: number): CompiledEntry<TAction, TData>;
7
+ export declare function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(rule: WardRule<TAction, TResource, TAttributes>, index: number): WardRule<TAction, TResource, TAttributes>;
36
8
  //# sourceMappingURL=_compile.d.ts.map
@@ -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":"AACA,OAAO,KAAK,EAAE,cAAc,EAAsB,QAAQ,EAAE,MAAM,SAAS,CAAC;AAyC5E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAO3F;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,SAAS,SAAS,MAAM,EAAE,WAAW,SAAS,cAAc,EAC9G,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,EAC/C,KAAK,EAAE,MAAM,GACZ,QAAQ,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,CA0C3C"}
package/dist/_compile.js CHANGED
@@ -1,43 +1,48 @@
1
- import "./constants.js";
2
1
  import { WardConfigError as e } from "./errors.js";
3
2
  //#region src/_compile.ts
4
- function t(t, n) {
5
- let r = `Rule[${n}]`, i = Array.isArray(t.role) ? t.role : [t.role];
6
- if (i.length === 0 || i.some((e) => typeof e != "string" || !e.trim())) throw new e(`${r}.role must be a non-empty string or non-empty array of strings`);
7
- if (typeof t.resource != "string" || !t.resource.trim()) throw new e(`${r}.resource must be a non-empty string`);
8
- if (t.resource.endsWith(":")) throw new e(`${r}.resource '${String(t.resource)}' ends with ':' — did you mean '${String(t.resource)}*'?`);
9
- if (typeof t.action != "string" || !t.action.trim()) throw new e(`${r}.action must be a non-empty string`);
10
- if (t.action.endsWith(":")) throw new e(`${r}.action '${String(t.action)}' ends with ':' — did you mean '${String(t.action)}*'?`);
11
- if (t.effect !== "allow" && t.effect !== "deny") throw new e(`${r}.effect must be "allow" or "deny"`);
12
- if (t.priority !== void 0 && (typeof t.priority != "number" || !Number.isFinite(t.priority))) throw new e(`${r}.priority must be a finite number`);
13
- if (t.when !== void 0 && typeof t.when != "function") throw new e(`${r}.when must be a function`);
3
+ function t(n, r, i) {
4
+ if (n === null || typeof n == "boolean" || typeof n == "string") return n;
5
+ if (typeof n == "number") {
6
+ if (!Number.isFinite(n)) throw new e(`${r} must contain only finite numbers`);
7
+ return n;
8
+ }
9
+ if (typeof n != "object") throw new e(`${r} must contain only JSON-compatible values`);
10
+ if (i.has(n)) throw new e(`${r} must not contain circular references`);
11
+ let a = Object.getPrototypeOf(n);
12
+ if (!Array.isArray(n) && a !== Object.prototype && a !== null) throw new e(`${r} must contain only arrays and plain objects`);
13
+ i.add(n);
14
+ try {
15
+ return Object.freeze(Array.isArray(n) ? Array.from(n, (e, n) => t(e, `${r}[${n}]`, i)) : Object.fromEntries(Object.keys(n).map((e) => [e, t(n[e], `${r}.${e}`, i)])));
16
+ } finally {
17
+ i.delete(n);
18
+ }
14
19
  }
15
- function n(e) {
16
- return e === "*" ? 0 : e.endsWith(":*") ? 1 : 2;
20
+ function n(n, r) {
21
+ if (n !== void 0) {
22
+ if (typeof n != "object" || !n || Array.isArray(n)) throw new e(`${r} must be a plain object`);
23
+ return t(n, r, /* @__PURE__ */ new Set());
24
+ }
17
25
  }
18
- function r(e) {
19
- return +!e.role.includes("*") + n(e.resource) + n(e.action);
20
- }
21
- function i(e, n) {
22
- t(e, n);
23
- let i = Array.isArray(e.role) ? [...e.role] : [e.role], a = Object.freeze([...new Set(i)]), o = Object.freeze({
24
- action: e.action,
25
- effect: e.effect,
26
- priority: e.priority ?? 0,
27
- resource: e.resource,
28
- role: a,
29
- ...e.when === void 0 ? {} : { when: e.when }
30
- }), s = r(o), c = o.priority;
31
- return {
32
- denyBonus: +(o.effect === "deny"),
33
- index: n,
34
- priority: c,
35
- roles: a,
36
- rule: o,
37
- score: s
38
- };
26
+ function r(t, r) {
27
+ let i = `Rule[${r}]`;
28
+ if (typeof t != "object" || !t || Array.isArray(t)) throw new e(`${i} must be a plain object`);
29
+ let a = Object.getPrototypeOf(t);
30
+ if (a !== Object.prototype && a !== null) throw new e(`${i} must be a plain object`);
31
+ if (typeof t.action != "string" || !t.action.trim()) throw new e(`${i}.action must be a non-empty string`);
32
+ if (t.action.endsWith(":")) throw new e(`${i}.action '${t.action}' ends with ':' — did you mean '${t.action}*'?`);
33
+ if (typeof t.resource != "string" || !t.resource.trim()) throw new e(`${i}.resource must be a non-empty string`);
34
+ if (t.resource.endsWith(":")) throw new e(`${i}.resource '${t.resource}' ends with ':' — did you mean '${t.resource}*'?`);
35
+ if (t.effect !== "allow" && t.effect !== "deny") throw new e(`${i}.effect must be "allow" or "deny"`);
36
+ if (t.condition !== void 0 && typeof t.condition != "function") throw new e(`${i}.condition must be a function`);
37
+ return Object.freeze({
38
+ action: t.action,
39
+ ...t.attributes === void 0 ? {} : { attributes: n(t.attributes, `${i}.attributes`) },
40
+ ...t.condition === void 0 ? {} : { condition: t.condition },
41
+ effect: t.effect,
42
+ resource: t.resource
43
+ });
39
44
  }
40
45
  //#endregion
41
- export { i as compileEntry, n as patternScore, t as validateRuleInput };
46
+ export { r as compileRule, n as snapshotAttributes };
42
47
 
43
48
  //# sourceMappingURL=_compile.js.map
@@ -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 { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':' — did you mean '${rule.action}*'?`);\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.endsWith(':')) {\n throw new WardConfigError(`${at}.resource '${rule.resource}' ends with ':' — did you mean '${rule.resource}*'?`);\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.condition !== undefined && typeof rule.condition !== 'function') {\n throw new WardConfigError(`${at}.condition must be a function`);\n }\n\n return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n"],"mappings":";;AAGA,SAAS,EAAc,GAAgB,GAAc,GAA4C;CAC/F,IAAI,MAAU,QAAQ,OAAO,KAAU,aAAa,OAAO,KAAU,UAAU,OAAO;CAEtF,IAAI,OAAO,KAAU,UAAU;EAC7B,IAAI,CAAC,OAAO,SAAS,CAAK,GAAG,MAAM,IAAI,EAAgB,GAAG,EAAK,kCAAkC;EACjG,OAAO;CACT;CAEA,IAAI,OAAO,KAAU,UACnB,MAAM,IAAI,EAAgB,GAAG,EAAK,0CAA0C;CAG9E,IAAI,EAAU,IAAI,CAAK,GAAG,MAAM,IAAI,EAAgB,GAAG,EAAK,sCAAsC;CAElG,IAAM,IAAY,OAAO,eAAe,CAAK;CAC7C,IAAI,CAAC,MAAM,QAAQ,CAAK,KAAK,MAAc,OAAO,aAAa,MAAc,MAC3E,MAAM,IAAI,EAAgB,GAAG,EAAK,4CAA4C;CAGhF,EAAU,IAAI,CAAK;CAEnB,IAAI;EAKF,OAHS,OAAO,OADZ,MAAM,QAAQ,CAAK,IACA,MAAM,KAAK,IAAQ,GAAM,MAAU,EAAc,GAAM,GAAG,EAAK,GAAG,EAAM,IAAI,CAAS,CAAC,IAI3G,OAAO,YACL,OAAO,KAAK,CAAK,CAAC,CAAC,KAAK,MAAQ,CAC9B,GACA,EAAe,EAAkC,IAAM,GAAG,EAAK,GAAG,KAAO,CAAS,CACpF,CAAC,CACH,CAT4G;CAWhH,UAAU;EACR,EAAU,OAAO,CAAK;CACxB;AACF;AAEA,SAAgB,EAAmB,GAAgB,GAA0C;CACvF,UAAU,KAAA,GACd;MAAI,OAAO,KAAU,aAAY,KAAkB,MAAM,QAAQ,CAAK,GACpE,MAAM,IAAI,EAAgB,GAAG,EAAK,wBAAwB;EAG5D,OAAO,EAAc,GAAO,mBAAM,IAAI,IAAI,CAAC;CAHiB;AAI9D;AAMA,SAAgB,EACd,GACA,GAC2C;CAC3C,IAAM,IAAK,QAAQ,EAAM;CAEzB,IAAI,OAAO,KAAS,aAAY,KAAiB,MAAM,QAAQ,CAAI,GACjE,MAAM,IAAI,EAAgB,GAAG,EAAG,wBAAwB;CAE1D,IAAM,IAAY,OAAO,eAAe,CAAI;CAC5C,IAAI,MAAc,OAAO,aAAa,MAAc,MAClD,MAAM,IAAI,EAAgB,GAAG,EAAG,wBAAwB;CAG1D,IAAI,OAAO,EAAK,UAAW,YAAY,CAAC,EAAK,OAAO,KAAK,GACvD,MAAM,IAAI,EAAgB,GAAG,EAAG,mCAAmC;CAGrE,IAAI,EAAK,OAAO,SAAS,GAAG,GAC1B,MAAM,IAAI,EAAgB,GAAG,EAAG,WAAW,EAAK,OAAO,kCAAkC,EAAK,OAAO,IAAI;CAG3G,IAAI,OAAO,EAAK,YAAa,YAAY,CAAC,EAAK,SAAS,KAAK,GAC3D,MAAM,IAAI,EAAgB,GAAG,EAAG,qCAAqC;CAGvE,IAAI,EAAK,SAAS,SAAS,GAAG,GAC5B,MAAM,IAAI,EAAgB,GAAG,EAAG,aAAa,EAAK,SAAS,kCAAkC,EAAK,SAAS,IAAI;CAGjH,IAAI,EAAK,WAAW,WAAW,EAAK,WAAW,QAC7C,MAAM,IAAI,EAAgB,GAAG,EAAG,kCAAkC;CAGpE,IAAI,EAAK,cAAc,KAAA,KAAa,OAAO,EAAK,aAAc,YAC5D,MAAM,IAAI,EAAgB,GAAG,EAAG,8BAA8B;CAGhE,OAAO,OAAO,OAAO;EACnB,QAAQ,EAAK;EACb,GAAI,EAAK,eAAe,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY,EAAmB,EAAK,YAAY,GAAG,EAAG,YAAY,EAAE;EAC/G,GAAI,EAAK,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,EAAK,UAAU;EACpE,QAAQ,EAAK;EACb,UAAU,EAAK;CACjB,CAAC;AACH"}
package/dist/_match.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./constants.cjs"),t=require("./errors.cjs"),n=require("./resource.cjs");function r(e){if(typeof e!=`object`||!e)throw new t.WardConfigError(`Invalid principal: expected { id: string, roles: string[] }`);let n=e;if(typeof n.id!=`string`||!n.id.trim())throw new t.WardConfigError(`Invalid principal: id must be a non-empty string`);if(!Array.isArray(n.roles)||n.roles.some(e=>typeof e!=`string`||!e.trim()))throw new t.WardConfigError(`Invalid principal: roles must be an array of non-empty strings`)}function i(e){e!==null&&r(e)}function a(t,n){return n===null?t.includes(e.ANONYMOUS):t.every(e=>e===`anonymous`)?!1:t.includes(`*`)?!0:t.some(e=>n.roles.includes(e))}function o(e,r,i,o,s,c=!1){if(!a(e.roles,r)||!n.matchesPattern(e.rule.resource,i)||o!==void 0&&!n.matchesPattern(e.rule.action,o))return!1;if(c||!e.rule.when)return!0;if(r===null)return!1;try{let t=e.rule.when({data:s,principal:r});if(t instanceof Promise)throw TypeError(`Rule[${e.index}] when() returned a Promise. Async predicates are not supported — use a synchronous predicate.`);return t}catch(n){throw new t.WardPredicateError(e.index,n)}}function s(e,t){return t.priority>e.priority||t.priority===e.priority&&t.score>e.score||t.priority===e.priority&&t.score===e.score&&t.denyBonus>e.denyBonus}function c(e,t,n,r,i){let a;for(let c of e)o(c,t,n,r,i)&&(!a||s(a,c))&&(a=c);return a}function l(e){return e?e.rule.effect===`deny`?{allowed:!1,reason:`explicit-deny`,rule:e.rule}:{allowed:!0,rule:e.rule}:{allowed:!1,reason:`no-matching-rule`}}exports.assertUserPrincipal=r,exports.isOverriddenBy=s,exports.matchesRule=o,exports.pickWinner=c,exports.principalMatchesRoles=a,exports.toDecision=l,exports.validatePrincipal=i;
1
+ const e=require("./errors.cjs"),t=require("./_compile.cjs"),n=require("./resource.cjs");function r(t){if(typeof t!=`object`||!t)throw new e.WardConfigError(`Invalid principal: expected { id: string, roles: string[] }`);let n=t;if(typeof n.id!=`string`||!n.id.trim())throw new e.WardConfigError(`Invalid principal: id must be a non-empty string`);if(!Array.isArray(n.roles)||n.roles.some(e=>typeof e!=`string`||!e.trim()))throw new e.WardConfigError(`Invalid principal: roles must be an array of non-empty strings`)}function i(e){return e==null?null:(r(e),Object.freeze({...e.attributes===void 0?{}:{attributes:t.snapshotAttributes(e.attributes,`Principal.attributes`)},id:e.id,roles:Object.freeze([...e.roles])}))}function a(n){if(typeof n.action!=`string`||!n.action.trim())throw new e.WardConfigError(`Decision.action must be a non-empty string`);if(typeof n.resource!=`string`||!n.resource.trim())throw new e.WardConfigError(`Decision.resource must be a non-empty string`);return Object.freeze({action:n.action,...n.attributes===void 0?{}:{attributes:t.snapshotAttributes(n.attributes,`Decision.attributes`)},principal:i(n.principal),resource:n.resource})}function o(e,t){if(Object.is(e,t))return!0;if(typeof e!=`object`||typeof t!=`object`||e===null||t===null)return!1;if(Array.isArray(e)||Array.isArray(t))return Array.isArray(e)&&Array.isArray(t)&&e.length===t.length&&e.every((e,n)=>o(e,t[n]));let n=e,r=t,i=Object.keys(n),a=Object.keys(r);return i.length===a.length&&i.every(e=>Object.hasOwn(r,e)&&o(n[e],r[e]))}function s(e,t){return e?t?Object.keys(e).every(n=>Object.hasOwn(t,n)&&o(e[n],t[n])):!1:!0}function c(e){return typeof e==`object`&&!!e&&`then`in e&&typeof e.then==`function`||typeof e==`function`&&`then`in e&&typeof e.then==`function`}function l(t,r,i){if(!n.matchesPattern(t.action,i.action)||!n.matchesPattern(t.resource,i.resource)||!s(t.attributes,i.attributes))return!1;if(!t.condition)return!0;try{let e=t.condition({attributes:i.attributes,principal:i.principal});if(c(e))throw TypeError(`Async conditions are not supported; return a boolean`);if(typeof e!=`boolean`)throw TypeError(`Conditions must return a boolean`);return e}catch(t){throw new e.WardConditionError(r,t)}}exports.assertUserPrincipal=r,exports.attributesMatch=s,exports.ruleMatches=l,exports.snapshotDecisionInput=a,exports.snapshotPrincipal=i;
2
2
  //# sourceMappingURL=_match.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"_match.cjs","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport { 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 { snapshotAttributes } from './_compile';\nimport { WardConditionError, WardConfigError } from './errors';\nimport { matchesPattern } from './resource';\nimport type {\n Principal,\n UserPrincipal,\n WardAttributes,\n WardAttributeValue,\n WardDecisionInput,\n WardRule,\n} from './types';\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 principal = input as Record<string, unknown>;\n\n if (typeof principal.id !== 'string' || !principal.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(principal.roles) || principal.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function snapshotPrincipal(principal: Principal | undefined): Principal {\n if (principal === undefined || principal === null) return null;\n\n assertUserPrincipal(principal);\n\n return Object.freeze({\n ...(principal.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(principal.attributes, 'Principal.attributes') }),\n id: principal.id,\n roles: Object.freeze([...principal.roles]),\n });\n}\n\nexport function snapshotDecisionInput<\n TAction extends string,\n TResource extends string,\n TAttributes extends WardAttributes,\n>(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n): WardDecisionInput<TAction, TResource, TAttributes> & {\n principal: Principal;\n} {\n if (typeof input.action !== 'string' || !input.action.trim()) {\n throw new WardConfigError('Decision.action must be a non-empty string');\n }\n if (typeof input.resource !== 'string' || !input.resource.trim()) {\n throw new WardConfigError('Decision.resource must be a non-empty string');\n }\n\n return Object.freeze({\n action: input.action,\n ...(input.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(input.attributes, 'Decision.attributes') as TAttributes }),\n principal: snapshotPrincipal(input.principal),\n resource: input.resource,\n });\n}\n\nfunction deepEqual(left: WardAttributeValue, right: WardAttributeValue): boolean {\n if (Object.is(left, right)) return true;\n if (typeof left !== 'object' || typeof right !== 'object' || left === null || right === null) return false;\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((value, index) => deepEqual(value, right[index]))\n );\n }\n\n const leftRecord = left as Record<string, WardAttributeValue>;\n const rightRecord = right as Record<string, WardAttributeValue>;\n const leftKeys = Object.keys(leftRecord);\n const rightKeys = Object.keys(rightRecord);\n\n return (\n leftKeys.length === rightKeys.length &&\n leftKeys.every((key) => Object.hasOwn(rightRecord, key) && deepEqual(leftRecord[key], rightRecord[key]))\n );\n}\n\nexport function attributesMatch(expected: WardAttributes | undefined, actual: WardAttributes | undefined): boolean {\n if (!expected) return true;\n if (!actual) return false;\n\n return Object.keys(expected).every((key) => Object.hasOwn(actual, key) && deepEqual(expected[key], actual[key]));\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n (typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function') ||\n (typeof value === 'function' && 'then' in value && typeof value.then === 'function')\n );\n}\n\nexport function ruleMatches<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n): boolean {\n if (!matchesPattern(rule.action, input.action)) return false;\n if (!matchesPattern(rule.resource, input.resource)) return false;\n if (!attributesMatch(rule.attributes, input.attributes)) return false;\n if (!rule.condition) return true;\n\n try {\n const result: unknown = rule.condition({ attributes: input.attributes, principal: input.principal });\n\n if (isThenable(result)) {\n throw new TypeError('Async conditions are not supported; return a boolean');\n }\n if (typeof result !== 'boolean') {\n throw new TypeError('Conditions must return a boolean');\n }\n\n return result;\n } catch (error) {\n throw new WardConditionError(index, error);\n }\n}\n"],"mappings":"wFAaA,SAAgB,EAAoB,EAAgD,CAClF,GAAI,OAAO,GAAU,UAAY,CAAC,EAChC,MAAM,IAAI,EAAA,gBAAgB,6DAA6D,EAGzF,IAAM,EAAY,EAElB,GAAI,OAAO,EAAU,IAAO,UAAY,CAAC,EAAU,GAAG,KAAK,EACzD,MAAM,IAAI,EAAA,gBAAgB,kDAAkD,EAG9E,GAAI,CAAC,MAAM,QAAQ,EAAU,KAAK,GAAK,EAAU,MAAM,KAAM,GAAS,OAAO,GAAS,UAAY,CAAC,EAAK,KAAK,CAAC,EAC5G,MAAM,IAAI,EAAA,gBAAgB,gEAAgE,CAE9F,CAEA,SAAgB,EAAkB,EAA6C,CAK7E,OAJI,GAAyC,KAAa,MAE1D,EAAoB,CAAS,EAEtB,OAAO,OAAO,CACnB,GAAI,EAAU,aAAe,IAAA,GACzB,CAAC,EACD,CAAE,WAAY,EAAA,mBAAmB,EAAU,WAAY,sBAAsB,CAAE,EACnF,GAAI,EAAU,GACd,MAAO,OAAO,OAAO,CAAC,GAAG,EAAU,KAAK,CAAC,CAC3C,CAAC,EACH,CAEA,SAAgB,EAKd,EAGA,CACA,GAAI,OAAO,EAAM,QAAW,UAAY,CAAC,EAAM,OAAO,KAAK,EACzD,MAAM,IAAI,EAAA,gBAAgB,4CAA4C,EAExE,GAAI,OAAO,EAAM,UAAa,UAAY,CAAC,EAAM,SAAS,KAAK,EAC7D,MAAM,IAAI,EAAA,gBAAgB,8CAA8C,EAG1E,OAAO,OAAO,OAAO,CACnB,OAAQ,EAAM,OACd,GAAI,EAAM,aAAe,IAAA,GACrB,CAAC,EACD,CAAE,WAAY,EAAA,mBAAmB,EAAM,WAAY,qBAAqB,CAAiB,EAC7F,UAAW,EAAkB,EAAM,SAAS,EAC5C,SAAU,EAAM,QAClB,CAAC,CACH,CAEA,SAAS,EAAU,EAA0B,EAAoC,CAC/E,GAAI,OAAO,GAAG,EAAM,CAAK,EAAG,MAAO,GACnC,GAAI,OAAO,GAAS,UAAY,OAAO,GAAU,UAAY,IAAS,MAAQ,IAAU,KAAM,MAAO,GACrG,GAAI,MAAM,QAAQ,CAAI,GAAK,MAAM,QAAQ,CAAK,EAC5C,OACE,MAAM,QAAQ,CAAI,GAClB,MAAM,QAAQ,CAAK,GACnB,EAAK,SAAW,EAAM,QACtB,EAAK,OAAO,EAAO,IAAU,EAAU,EAAO,EAAM,EAAM,CAAC,EAI/D,IAAM,EAAa,EACb,EAAc,EACd,EAAW,OAAO,KAAK,CAAU,EACjC,EAAY,OAAO,KAAK,CAAW,EAEzC,OACE,EAAS,SAAW,EAAU,QAC9B,EAAS,MAAO,GAAQ,OAAO,OAAO,EAAa,CAAG,GAAK,EAAU,EAAW,GAAM,EAAY,EAAI,CAAC,CAE3G,CAEA,SAAgB,EAAgB,EAAsC,EAA6C,CAIjH,OAHK,EACA,EAEE,OAAO,KAAK,CAAQ,CAAC,CAAC,MAAO,GAAQ,OAAO,OAAO,EAAQ,CAAG,GAAK,EAAU,EAAS,GAAM,EAAO,EAAI,CAAC,EAF3F,GADE,EAIxB,CAEA,SAAS,EAAW,EAA+C,CACjE,OACG,OAAO,GAAU,YAAY,GAAkB,SAAU,GAAS,OAAO,EAAM,MAAS,YACxF,OAAO,GAAU,YAAc,SAAU,GAAS,OAAO,EAAM,MAAS,UAE7E,CAEA,SAAgB,EACd,EACA,EACA,EACS,CAGT,GAFI,CAAC,EAAA,eAAe,EAAK,OAAQ,EAAM,MAAM,GACzC,CAAC,EAAA,eAAe,EAAK,SAAU,EAAM,QAAQ,GAC7C,CAAC,EAAgB,EAAK,WAAY,EAAM,UAAU,EAAG,MAAO,GAChE,GAAI,CAAC,EAAK,UAAW,MAAO,GAE5B,GAAI,CACF,IAAM,EAAkB,EAAK,UAAU,CAAE,WAAY,EAAM,WAAY,UAAW,EAAM,SAAU,CAAC,EAEnG,GAAI,EAAW,CAAM,EACnB,MAAU,UAAU,sDAAsD,EAE5E,GAAI,OAAO,GAAW,UACpB,MAAU,UAAU,kCAAkC,EAGxD,OAAO,CACT,OAAS,EAAO,CACd,MAAM,IAAI,EAAA,mBAAmB,EAAO,CAAK,CAC3C,CACF"}
package/dist/_match.d.ts CHANGED
@@ -1,22 +1,12 @@
1
- import type { CompiledEntry } from './_compile';
2
- import type { Principal, UserPrincipal, WardDecision } from './types';
1
+ import type { Principal, UserPrincipal, WardAttributes, WardDecisionInput, WardRule } from './types';
3
2
  /** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */
4
3
  export declare function assertUserPrincipal(input: unknown): asserts input is UserPrincipal;
5
- export declare function validatePrincipal(principal: Principal): void;
6
- /** Returns true if the principal's role set intersects with the rule's roles. */
7
- export declare function principalMatchesRoles(roles: readonly string[], principal: Principal): boolean;
8
- /**
9
- * Check whether a rule applies to a given request.
10
- *
11
- * @param action - Pass `undefined` to skip the action check (used by `rulesInScope`).
12
- * @param data - Data payload; passed to `when` predicates.
13
- * @param skipPredicate - When true, omit the `when` evaluation even when data is present.
14
- */
15
- export declare function matchesRule<TAction extends string, TData>(entry: CompiledEntry<TAction, TData>, principal: Principal, resource: string, action: TAction | undefined, data: TData | undefined, skipPredicate?: boolean): boolean;
16
- /**
17
- * Returns true if `challenger` would displace `current` as the pickWinner result.
18
- */
19
- export declare function isOverriddenBy<TAction extends string, TData>(current: CompiledEntry<TAction, TData>, challenger: CompiledEntry<TAction, TData>): boolean;
20
- export declare function pickWinner<TAction extends string, TData>(entries: CompiledEntry<TAction, TData>[], principal: Principal, resource: string, action: TAction, data: TData | undefined): CompiledEntry<TAction, TData> | undefined;
21
- export declare function toDecision<TAction extends string, TData>(winner: CompiledEntry<TAction, TData> | undefined): WardDecision<TAction, TData>;
4
+ export declare function snapshotPrincipal(principal: Principal | undefined): Principal;
5
+ export declare function snapshotDecisionInput<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(input: WardDecisionInput<TAction, TResource, TAttributes>): WardDecisionInput<TAction, TResource, TAttributes> & {
6
+ principal: Principal;
7
+ };
8
+ export declare function attributesMatch(expected: WardAttributes | undefined, actual: WardAttributes | undefined): boolean;
9
+ export declare function ruleMatches<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(rule: WardRule<TAction, TResource, TAttributes>, index: number, input: WardDecisionInput<TAction, TResource, TAttributes> & {
10
+ principal: Principal;
11
+ }): boolean;
22
12
  //# sourceMappingURL=_match.d.ts.map
@@ -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":"AAGA,OAAO,KAAK,EACV,SAAS,EACT,aAAa,EACb,cAAc,EAEd,iBAAiB,EACjB,QAAQ,EACT,MAAM,SAAS,CAAC;AAEjB,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAclF;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAY7E;AAED,wBAAgB,qBAAqB,CACnC,OAAO,SAAS,MAAM,EACtB,SAAS,SAAS,MAAM,EACxB,WAAW,SAAS,cAAc,EAElC,KAAK,EAAE,iBAAiB,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,GACxD,iBAAiB,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,GAAG;IACtD,SAAS,EAAE,SAAS,CAAC;CACtB,CAgBA;AAyBD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,EAAE,MAAM,EAAE,cAAc,GAAG,SAAS,GAAG,OAAO,CAKjH;AASD,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,SAAS,SAAS,MAAM,EAAE,WAAW,SAAS,cAAc,EAC9G,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,EAC/C,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,iBAAiB,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,GAAG;IAAE,SAAS,EAAE,SAAS,CAAA;CAAE,GACnF,OAAO,CAoBT"}
package/dist/_match.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ANONYMOUS as e } from "./constants.js";
2
- import { WardConfigError as t, WardPredicateError as n } from "./errors.js";
1
+ import { WardConditionError as e, WardConfigError as t } from "./errors.js";
2
+ import { snapshotAttributes as n } from "./_compile.js";
3
3
  import { matchesPattern as r } from "./resource.js";
4
4
  //#region src/_match.ts
5
5
  function i(e) {
@@ -9,48 +9,51 @@ function i(e) {
9
9
  if (!Array.isArray(n.roles) || n.roles.some((e) => typeof e != "string" || !e.trim())) throw new t("Invalid principal: roles must be an array of non-empty strings");
10
10
  }
11
11
  function a(e) {
12
- e !== null && i(e);
12
+ return e == null ? null : (i(e), Object.freeze({
13
+ ...e.attributes === void 0 ? {} : { attributes: n(e.attributes, "Principal.attributes") },
14
+ id: e.id,
15
+ roles: Object.freeze([...e.roles])
16
+ }));
13
17
  }
14
- function o(t, n) {
15
- return n === null ? t.includes(e) : t.every((e) => e === "anonymous") ? !1 : t.includes("*") ? !0 : t.some((e) => n.roles.includes(e));
18
+ function o(e) {
19
+ if (typeof e.action != "string" || !e.action.trim()) throw new t("Decision.action must be a non-empty string");
20
+ if (typeof e.resource != "string" || !e.resource.trim()) throw new t("Decision.resource must be a non-empty string");
21
+ return Object.freeze({
22
+ action: e.action,
23
+ ...e.attributes === void 0 ? {} : { attributes: n(e.attributes, "Decision.attributes") },
24
+ principal: a(e.principal),
25
+ resource: e.resource
26
+ });
16
27
  }
17
- function s(e, t, i, a, s, c = !1) {
18
- if (!o(e.roles, t) || !r(e.rule.resource, i) || a !== void 0 && !r(e.rule.action, a)) return !1;
19
- if (c || !e.rule.when) return !0;
20
- if (t === null) return !1;
21
- try {
22
- let n = e.rule.when({
23
- data: s,
24
- principal: t
25
- });
26
- if (n instanceof Promise) throw TypeError(`Rule[${e.index}] when() returned a Promise. Async predicates are not supported — use a synchronous predicate.`);
27
- return n;
28
- } catch (t) {
29
- throw new n(e.index, t);
30
- }
28
+ function s(e, t) {
29
+ if (Object.is(e, t)) return !0;
30
+ if (typeof e != "object" || typeof t != "object" || e === null || t === null) return !1;
31
+ if (Array.isArray(e) || Array.isArray(t)) return Array.isArray(e) && Array.isArray(t) && e.length === t.length && e.every((e, n) => s(e, t[n]));
32
+ let n = e, r = t, i = Object.keys(n), a = Object.keys(r);
33
+ return i.length === a.length && i.every((e) => Object.hasOwn(r, e) && s(n[e], r[e]));
31
34
  }
32
35
  function c(e, t) {
33
- return t.priority > e.priority || t.priority === e.priority && t.score > e.score || t.priority === e.priority && t.score === e.score && t.denyBonus > e.denyBonus;
36
+ return e ? t ? Object.keys(e).every((n) => Object.hasOwn(t, n) && s(e[n], t[n])) : !1 : !0;
34
37
  }
35
- function l(e, t, n, r, i) {
36
- let a;
37
- for (let o of e) s(o, t, n, r, i) && (!a || c(a, o)) && (a = o);
38
- return a;
38
+ function l(e) {
39
+ return typeof e == "object" && !!e && "then" in e && typeof e.then == "function" || typeof e == "function" && "then" in e && typeof e.then == "function";
39
40
  }
40
- function u(e) {
41
- return e ? e.rule.effect === "deny" ? {
42
- allowed: !1,
43
- reason: "explicit-deny",
44
- rule: e.rule
45
- } : {
46
- allowed: !0,
47
- rule: e.rule
48
- } : {
49
- allowed: !1,
50
- reason: "no-matching-rule"
51
- };
41
+ function u(t, n, i) {
42
+ if (!r(t.action, i.action) || !r(t.resource, i.resource) || !c(t.attributes, i.attributes)) return !1;
43
+ if (!t.condition) return !0;
44
+ try {
45
+ let e = t.condition({
46
+ attributes: i.attributes,
47
+ principal: i.principal
48
+ });
49
+ if (l(e)) throw TypeError("Async conditions are not supported; return a boolean");
50
+ if (typeof e != "boolean") throw TypeError("Conditions must return a boolean");
51
+ return e;
52
+ } catch (t) {
53
+ throw new e(n, t);
54
+ }
52
55
  }
53
56
  //#endregion
54
- export { i as assertUserPrincipal, c as isOverriddenBy, s as matchesRule, l as pickWinner, o as principalMatchesRoles, u as toDecision, a as validatePrincipal };
57
+ export { i as assertUserPrincipal, c as attributesMatch, u as ruleMatches, o as snapshotDecisionInput, a as snapshotPrincipal };
55
58
 
56
59
  //# sourceMappingURL=_match.js.map