@vielzeug/ward 1.0.2
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 +81 -0
- package/dist/_compile.cjs +2 -0
- package/dist/_compile.cjs.map +1 -0
- package/dist/_compile.d.ts +36 -0
- package/dist/_compile.d.ts.map +1 -0
- package/dist/_compile.js +43 -0
- package/dist/_compile.js.map +1 -0
- package/dist/_conflict.cjs +2 -0
- package/dist/_conflict.cjs.map +1 -0
- package/dist/_conflict.d.ts +10 -0
- package/dist/_conflict.d.ts.map +1 -0
- package/dist/_conflict.js +51 -0
- package/dist/_conflict.js.map +1 -0
- package/dist/_dev.cjs +2 -0
- package/dist/_dev.cjs.map +1 -0
- package/dist/_dev.d.ts +2 -0
- package/dist/_dev.d.ts.map +1 -0
- package/dist/_dev.js +9 -0
- package/dist/_dev.js.map +1 -0
- package/dist/_match.cjs +2 -0
- package/dist/_match.cjs.map +1 -0
- package/dist/_match.d.ts +22 -0
- package/dist/_match.d.ts.map +1 -0
- package/dist/_match.js +56 -0
- package/dist/_match.js.map +1 -0
- package/dist/builder.cjs +2 -0
- package/dist/builder.cjs.map +1 -0
- package/dist/builder.d.ts +90 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +42 -0
- package/dist/builder.js.map +1 -0
- package/dist/constants.cjs +2 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -0
- package/dist/devtools.cjs +2 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.ts +36 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +16 -0
- package/dist/devtools.js.map +1 -0
- package/dist/errors.cjs +2 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +19 -0
- package/dist/errors.js.map +1 -0
- package/dist/factory.cjs +2 -0
- package/dist/factory.cjs.map +1 -0
- package/dist/factory.d.ts +14 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +107 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/middleware.cjs +2 -0
- package/dist/middleware.cjs.map +1 -0
- package/dist/middleware.d.ts +37 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +20 -0
- package/dist/middleware.js.map +1 -0
- package/dist/resource.cjs +2 -0
- package/dist/resource.cjs.map +1 -0
- package/dist/resource.d.ts +41 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +17 -0
- package/dist/resource.js.map +1 -0
- package/dist/types.d.ts +229 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/ward.cjs +2 -0
- package/dist/ward.cjs.map +1 -0
- package/dist/ward.iife.js +2 -0
- package/dist/ward.iife.js.map +1 -0
- package/dist/ward.js +2 -0
- package/dist/ward.js.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @vielzeug/ward
|
|
2
|
+
|
|
3
|
+
> Minimal authorization engine with deterministic precedence, wildcard support, and runtime predicates.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@vielzeug/ward) [](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
<details>
|
|
8
|
+
<summary>Quick Reference</summary>
|
|
9
|
+
|
|
10
|
+
**Package:** `@vielzeug/ward` · **Category:** Auth
|
|
11
|
+
|
|
12
|
+
**Key exports:** `createWard`, `allow`, `deny`, `predicate`, `owns`, `matchesPattern`, `patternCovers`, `guardRequest`, `guardRequestWith`, `WardPredicateError`, `WILDCARD`, `ANONYMOUS`
|
|
13
|
+
|
|
14
|
+
**When to use:** Minimal authorization engine with deterministic precedence, wildcard support, and runtime predicates.
|
|
15
|
+
|
|
16
|
+
**Related:** [@vielzeug/rune](https://vielzeug.dev/rune/) · [@vielzeug/wayfinder](https://vielzeug.dev/wayfinder/) · [@vielzeug/conduit](https://vielzeug.dev/conduit/)
|
|
17
|
+
|
|
18
|
+
</details>
|
|
19
|
+
|
|
20
|
+
`@vielzeug/ward` is part of Vielzeug and ships as a zero-dependency TypeScript package with ESM+CJS output.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm add @vielzeug/ward
|
|
26
|
+
npm install @vielzeug/ward
|
|
27
|
+
yarn add @vielzeug/ward
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { ANONYMOUS, WILDCARD, allow, createWard, deny, predicate } from '@vielzeug/ward';
|
|
34
|
+
|
|
35
|
+
const ward = createWard<'read' | 'update', { authorId: string }>([
|
|
36
|
+
// Multi-role rule: viewer and editor can both read
|
|
37
|
+
...allow(['viewer', 'editor'], 'posts', ['read']),
|
|
38
|
+
// Editor can update their own posts (ownership predicate)
|
|
39
|
+
...allow('editor', 'posts', ['update'], { when: predicate.owns('authorId') }),
|
|
40
|
+
// High-priority deny overrides any allow rule for blocked principals
|
|
41
|
+
...deny('blocked', WILDCARD, [WILDCARD], { priority: 100 }),
|
|
42
|
+
// Anonymous visitors can read posts
|
|
43
|
+
...allow(ANONYMOUS, 'posts', ['read']),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const principal = { id: 'u1', roles: ['editor'] };
|
|
47
|
+
|
|
48
|
+
// Full decision object — narrow on .allowed for type-safe access
|
|
49
|
+
const decision = ward.explain(principal, 'posts', 'update', { authorId: 'u2' });
|
|
50
|
+
if (!decision.allowed) console.log(decision.reason); // 'no-matching-rule' | 'explicit-deny'
|
|
51
|
+
|
|
52
|
+
// Batch decisions across multiple resources/actions in one call
|
|
53
|
+
const results = ward.checkAll(principal, [
|
|
54
|
+
{ resource: 'posts', action: 'read' },
|
|
55
|
+
{ resource: 'posts', action: 'update', data: { authorId: 'u1' } },
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
// Decision trace — candidates with index, score, priority, won (no logger fired)
|
|
59
|
+
const trace = ward.trace(principal, 'posts', 'update', { authorId: 'u2' });
|
|
60
|
+
trace.candidates.forEach((c) => console.log(`Rule[${c.index}]`, c.rule.effect, c.score, c.won));
|
|
61
|
+
|
|
62
|
+
// Principal-bound view — principal is snapshotted at bind time
|
|
63
|
+
const bound = ward.forUser(principal);
|
|
64
|
+
bound.allowedActions('posts', ['read', 'update', 'delete']);
|
|
65
|
+
bound.explain('posts', 'update', { authorId: 'u2' });
|
|
66
|
+
|
|
67
|
+
// Conflict detection — O(n²), lazy + cached
|
|
68
|
+
const conflicts = ward.detectConflicts();
|
|
69
|
+
if (conflicts.length > 0) console.warn('Policy conflicts:', conflicts);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Documentation
|
|
73
|
+
|
|
74
|
+
- [Overview](https://vielzeug.dev/ward/)
|
|
75
|
+
- [Usage Guide](https://vielzeug.dev/ward/usage)
|
|
76
|
+
- [API Reference](https://vielzeug.dev/ward/api)
|
|
77
|
+
- [Examples](https://vielzeug.dev/ward/examples)
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
|
|
@@ -0,0 +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;
|
|
2
|
+
//# sourceMappingURL=_compile.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_compile.cjs","names":[],"sources":["../src/_compile.ts"],"sourcesContent":["import type { WardRule } from './types';\n\nimport { WILDCARD } from './constants';\nimport { WardConfigError } from './errors';\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":"2DAuCA,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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { WardRule } from './types';
|
|
2
|
+
import { WILDCARD } from './constants';
|
|
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;
|
|
28
|
+
/**
|
|
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
|
|
33
|
+
*/
|
|
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>;
|
|
36
|
+
//# sourceMappingURL=_compile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_compile.d.ts","sourceRoot":"","sources":["../src/_compile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAOvC,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"}
|
package/dist/_compile.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import "./constants.js";
|
|
2
|
+
import { WardConfigError as e } from "./errors.js";
|
|
3
|
+
//#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`);
|
|
14
|
+
}
|
|
15
|
+
function n(e) {
|
|
16
|
+
return e === "*" ? 0 : e.endsWith(":*") ? 1 : 2;
|
|
17
|
+
}
|
|
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
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { i as compileEntry, n as patternScore, t as validateRuleInput };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=_compile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_compile.js","names":[],"sources":["../src/_compile.ts"],"sourcesContent":["import type { WardRule } from './types';\n\nimport { WILDCARD } from './constants';\nimport { WardConfigError } from './errors';\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":";;;AAuCA,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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./constants.cjs"),t=require("./resource.cjs"),n=require("./_match.cjs");function r(t,n){let r=n.includes(e.ANONYMOUS),i=n.some(t=>t!==e.ANONYMOUS);return r&&!t.includes(`anonymous`)?!1:i?t.includes(`*`)?!0:n.filter(t=>t!==e.ANONYMOUS).every(e=>t.includes(e)):!0}function i(e,t){if(e.length!==t.length)return!1;let n=new Set(e);return t.every(e=>n.has(e))}function a(e,n){return r(e.roles,n.roles)&&t.patternCovers(e.rule.resource,n.rule.resource)&&t.patternCovers(e.rule.action,n.rule.action)}function o(e,t){let r=[];for(let o=0;o<e.length&&r.length<t;o++)for(let s=o+1;s<e.length&&r.length<t;s++){let t=e[o],c=e[s];if(!t.rule.when&&!c.rule.when&&i(t.roles,c.roles)&&t.rule.resource===c.rule.resource&&t.rule.action===c.rule.action){r.push({indexA:t.index,indexB:c.index,kind:`duplicate`,ruleA:t.rule,ruleB:c.rule});continue}let l=n.isOverriddenBy(t,c);!l&&!t.rule.when&&a(t,c)?r.push({kind:`shadowed`,shadowedIndex:c.index,shadowedRule:c.rule,shadowingIndex:t.index,shadowingRule:t.rule}):l&&!c.rule.when&&a(c,t)&&r.push({kind:`shadowed`,shadowedIndex:t.index,shadowedRule:t.rule,shadowingIndex:c.index,shadowingRule:c.rule})}return r}exports.computeConflicts=o;
|
|
2
|
+
//# sourceMappingURL=_conflict.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_conflict.cjs","names":[],"sources":["../src/_conflict.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type { WardConflict } from './types';\n\nimport { isOverriddenBy } from './_match';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { patternCovers } from './resource';\n\n// ---------------------------------------------------------------------------\n// Role coverage helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if every principal that satisfies `narrowRoles` also satisfies `broadRoles`.\n */\nfunction roleCoversRoles(broadRoles: readonly string[], narrowRoles: readonly string[]): boolean {\n const narrowHasAnonymous = narrowRoles.includes(ANONYMOUS);\n const narrowHasAuthenticated = narrowRoles.some((r) => r !== ANONYMOUS);\n\n if (narrowHasAnonymous && !broadRoles.includes(ANONYMOUS)) return false;\n\n if (narrowHasAuthenticated) {\n if (broadRoles.includes(WILDCARD)) return true;\n\n return narrowRoles.filter((r) => r !== ANONYMOUS).every((r) => broadRoles.includes(r));\n }\n\n return true;\n}\n\nfunction rolesSetsEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n\n const setA = new Set(a);\n\n return b.every((r) => setA.has(r));\n}\n\n/** Returns true if every request that triggers `narrow` would also trigger `broad`. */\nfunction ruleCovers<TAction extends string, TData>(\n broad: CompiledEntry<TAction, TData>,\n narrow: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n roleCoversRoles(broad.roles, narrow.roles) &&\n patternCovers(broad.rule.resource as string, narrow.rule.resource as string) &&\n patternCovers(broad.rule.action as string, narrow.rule.action as string)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Conflict detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detects conflicts between compiled entries.\n *\n * @complexity O(n²) in the number of rules. Scales to hundreds of rules\n * with negligible overhead; for very large auto-generated policies, use `maxConflicts`.\n */\nexport function computeConflicts<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n maxConflicts: number,\n): WardConflict<TAction, TData>[] {\n const conflicts: WardConflict<TAction, TData>[] = [];\n\n for (let i = 0; i < entries.length && conflicts.length < maxConflicts; i++) {\n for (let j = i + 1; j < entries.length && conflicts.length < maxConflicts; j++) {\n const a = entries[i];\n const b = entries[j];\n\n if (\n !a.rule.when &&\n !b.rule.when &&\n rolesSetsEqual(a.roles, b.roles) &&\n a.rule.resource === b.rule.resource &&\n a.rule.action === b.rule.action\n ) {\n conflicts.push({\n indexA: a.index,\n indexB: b.index,\n kind: 'duplicate',\n ruleA: a.rule,\n ruleB: b.rule,\n });\n continue;\n }\n\n const bCanOverrideA = isOverriddenBy(a, b);\n\n if (!bCanOverrideA && !a.rule.when && ruleCovers(a, b)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: b.index,\n shadowedRule: b.rule,\n shadowingIndex: a.index,\n shadowingRule: a.rule,\n });\n } else if (bCanOverrideA && !b.rule.when && ruleCovers(b, a)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: a.index,\n shadowedRule: a.rule,\n shadowingIndex: b.index,\n shadowingRule: b.rule,\n });\n }\n }\n }\n\n return conflicts;\n}\n"],"mappings":"yFAcA,SAAS,EAAgB,EAA+B,EAAyC,CAC/F,IAAM,EAAqB,EAAY,SAAS,EAAA,SAAS,EACnD,EAAyB,EAAY,KAAM,GAAM,IAAM,EAAA,SAAS,EAUtE,OARI,GAAsB,CAAC,EAAW,SAAA,WAAkB,EAAU,GAE9D,EACE,EAAW,SAAA,GAAiB,EAAU,GAEnC,EAAY,OAAQ,GAAM,IAAM,EAAA,SAAS,CAAC,CAAC,MAAO,GAAM,EAAW,SAAS,CAAC,CAAC,EAGhF,EACT,CAEA,SAAS,EAAe,EAAsB,EAA+B,CAC3E,GAAI,EAAE,SAAW,EAAE,OAAQ,MAAO,GAElC,IAAM,EAAO,IAAI,IAAI,CAAC,EAEtB,OAAO,EAAE,MAAO,GAAM,EAAK,IAAI,CAAC,CAAC,CACnC,CAGA,SAAS,EACP,EACA,EACS,CACT,OACE,EAAgB,EAAM,MAAO,EAAO,KAAK,GACzC,EAAA,cAAc,EAAM,KAAK,SAAoB,EAAO,KAAK,QAAkB,GAC3E,EAAA,cAAc,EAAM,KAAK,OAAkB,EAAO,KAAK,MAAgB,CAE3E,CAYA,SAAgB,EACd,EACA,EACgC,CAChC,IAAM,EAA4C,CAAC,EAEnD,IAAK,IAAI,EAAI,EAAG,EAAI,EAAQ,QAAU,EAAU,OAAS,EAAc,IACrE,IAAK,IAAI,EAAI,EAAI,EAAG,EAAI,EAAQ,QAAU,EAAU,OAAS,EAAc,IAAK,CAC9E,IAAM,EAAI,EAAQ,GACZ,EAAI,EAAQ,GAElB,GACE,CAAC,EAAE,KAAK,MACR,CAAC,EAAE,KAAK,MACR,EAAe,EAAE,MAAO,EAAE,KAAK,GAC/B,EAAE,KAAK,WAAa,EAAE,KAAK,UAC3B,EAAE,KAAK,SAAW,EAAE,KAAK,OACzB,CACA,EAAU,KAAK,CACb,OAAQ,EAAE,MACV,OAAQ,EAAE,MACV,KAAM,YACN,MAAO,EAAE,KACT,MAAO,EAAE,IACX,CAAC,EACD,QACF,CAEA,IAAM,EAAgB,EAAA,eAAe,EAAG,CAAC,EAErC,CAAC,GAAiB,CAAC,EAAE,KAAK,MAAQ,EAAW,EAAG,CAAC,EACnD,EAAU,KAAK,CACb,KAAM,WACN,cAAe,EAAE,MACjB,aAAc,EAAE,KAChB,eAAgB,EAAE,MAClB,cAAe,EAAE,IACnB,CAAC,EACQ,GAAiB,CAAC,EAAE,KAAK,MAAQ,EAAW,EAAG,CAAC,GACzD,EAAU,KAAK,CACb,KAAM,WACN,cAAe,EAAE,MACjB,aAAc,EAAE,KAChB,eAAgB,EAAE,MAClB,cAAe,EAAE,IACnB,CAAC,CAEL,CAGF,OAAO,CACT"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CompiledEntry } from './_compile';
|
|
2
|
+
import type { WardConflict } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Detects conflicts between compiled entries.
|
|
5
|
+
*
|
|
6
|
+
* @complexity O(n²) in the number of rules. Scales to hundreds of rules
|
|
7
|
+
* with negligible overhead; for very large auto-generated policies, use `maxConflicts`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function computeConflicts<TAction extends string, TData>(entries: CompiledEntry<TAction, TData>[], maxConflicts: number): WardConflict<TAction, TData>[];
|
|
10
|
+
//# sourceMappingURL=_conflict.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_conflict.d.ts","sourceRoot":"","sources":["../src/_conflict.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAoD5C;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EAC5D,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EACxC,YAAY,EAAE,MAAM,GACnB,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAgDhC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ANONYMOUS as e } from "./constants.js";
|
|
2
|
+
import { patternCovers as t } from "./resource.js";
|
|
3
|
+
import { isOverriddenBy as n } from "./_match.js";
|
|
4
|
+
//#region src/_conflict.ts
|
|
5
|
+
function r(t, n) {
|
|
6
|
+
let r = n.includes(e), i = n.some((t) => t !== e);
|
|
7
|
+
return r && !t.includes("anonymous") ? !1 : i ? t.includes("*") ? !0 : n.filter((t) => t !== e).every((e) => t.includes(e)) : !0;
|
|
8
|
+
}
|
|
9
|
+
function i(e, t) {
|
|
10
|
+
if (e.length !== t.length) return !1;
|
|
11
|
+
let n = new Set(e);
|
|
12
|
+
return t.every((e) => n.has(e));
|
|
13
|
+
}
|
|
14
|
+
function a(e, n) {
|
|
15
|
+
return r(e.roles, n.roles) && t(e.rule.resource, n.rule.resource) && t(e.rule.action, n.rule.action);
|
|
16
|
+
}
|
|
17
|
+
function o(e, t) {
|
|
18
|
+
let r = [];
|
|
19
|
+
for (let o = 0; o < e.length && r.length < t; o++) for (let s = o + 1; s < e.length && r.length < t; s++) {
|
|
20
|
+
let t = e[o], c = e[s];
|
|
21
|
+
if (!t.rule.when && !c.rule.when && i(t.roles, c.roles) && t.rule.resource === c.rule.resource && t.rule.action === c.rule.action) {
|
|
22
|
+
r.push({
|
|
23
|
+
indexA: t.index,
|
|
24
|
+
indexB: c.index,
|
|
25
|
+
kind: "duplicate",
|
|
26
|
+
ruleA: t.rule,
|
|
27
|
+
ruleB: c.rule
|
|
28
|
+
});
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
let l = n(t, c);
|
|
32
|
+
!l && !t.rule.when && a(t, c) ? r.push({
|
|
33
|
+
kind: "shadowed",
|
|
34
|
+
shadowedIndex: c.index,
|
|
35
|
+
shadowedRule: c.rule,
|
|
36
|
+
shadowingIndex: t.index,
|
|
37
|
+
shadowingRule: t.rule
|
|
38
|
+
}) : l && !c.rule.when && a(c, t) && r.push({
|
|
39
|
+
kind: "shadowed",
|
|
40
|
+
shadowedIndex: t.index,
|
|
41
|
+
shadowedRule: t.rule,
|
|
42
|
+
shadowingIndex: c.index,
|
|
43
|
+
shadowingRule: c.rule
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return r;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { o as computeConflicts };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=_conflict.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_conflict.js","names":[],"sources":["../src/_conflict.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type { WardConflict } from './types';\n\nimport { isOverriddenBy } from './_match';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { patternCovers } from './resource';\n\n// ---------------------------------------------------------------------------\n// Role coverage helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if every principal that satisfies `narrowRoles` also satisfies `broadRoles`.\n */\nfunction roleCoversRoles(broadRoles: readonly string[], narrowRoles: readonly string[]): boolean {\n const narrowHasAnonymous = narrowRoles.includes(ANONYMOUS);\n const narrowHasAuthenticated = narrowRoles.some((r) => r !== ANONYMOUS);\n\n if (narrowHasAnonymous && !broadRoles.includes(ANONYMOUS)) return false;\n\n if (narrowHasAuthenticated) {\n if (broadRoles.includes(WILDCARD)) return true;\n\n return narrowRoles.filter((r) => r !== ANONYMOUS).every((r) => broadRoles.includes(r));\n }\n\n return true;\n}\n\nfunction rolesSetsEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n\n const setA = new Set(a);\n\n return b.every((r) => setA.has(r));\n}\n\n/** Returns true if every request that triggers `narrow` would also trigger `broad`. */\nfunction ruleCovers<TAction extends string, TData>(\n broad: CompiledEntry<TAction, TData>,\n narrow: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n roleCoversRoles(broad.roles, narrow.roles) &&\n patternCovers(broad.rule.resource as string, narrow.rule.resource as string) &&\n patternCovers(broad.rule.action as string, narrow.rule.action as string)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Conflict detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detects conflicts between compiled entries.\n *\n * @complexity O(n²) in the number of rules. Scales to hundreds of rules\n * with negligible overhead; for very large auto-generated policies, use `maxConflicts`.\n */\nexport function computeConflicts<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n maxConflicts: number,\n): WardConflict<TAction, TData>[] {\n const conflicts: WardConflict<TAction, TData>[] = [];\n\n for (let i = 0; i < entries.length && conflicts.length < maxConflicts; i++) {\n for (let j = i + 1; j < entries.length && conflicts.length < maxConflicts; j++) {\n const a = entries[i];\n const b = entries[j];\n\n if (\n !a.rule.when &&\n !b.rule.when &&\n rolesSetsEqual(a.roles, b.roles) &&\n a.rule.resource === b.rule.resource &&\n a.rule.action === b.rule.action\n ) {\n conflicts.push({\n indexA: a.index,\n indexB: b.index,\n kind: 'duplicate',\n ruleA: a.rule,\n ruleB: b.rule,\n });\n continue;\n }\n\n const bCanOverrideA = isOverriddenBy(a, b);\n\n if (!bCanOverrideA && !a.rule.when && ruleCovers(a, b)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: b.index,\n shadowedRule: b.rule,\n shadowingIndex: a.index,\n shadowingRule: a.rule,\n });\n } else if (bCanOverrideA && !b.rule.when && ruleCovers(b, a)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: a.index,\n shadowedRule: a.rule,\n shadowingIndex: b.index,\n shadowingRule: b.rule,\n });\n }\n }\n }\n\n return conflicts;\n}\n"],"mappings":";;;;AAcA,SAAS,EAAgB,GAA+B,GAAyC;CAC/F,IAAM,IAAqB,EAAY,SAAS,CAAS,GACnD,IAAyB,EAAY,MAAM,MAAM,MAAM,CAAS;CAUtE,OARI,KAAsB,CAAC,EAAW,SAAA,WAAkB,IAAU,KAE9D,IACE,EAAW,SAAA,GAAiB,IAAU,KAEnC,EAAY,QAAQ,MAAM,MAAM,CAAS,CAAC,CAAC,OAAO,MAAM,EAAW,SAAS,CAAC,CAAC,IAGhF;AACT;AAEA,SAAS,EAAe,GAAsB,GAA+B;CAC3E,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAElC,IAAM,IAAO,IAAI,IAAI,CAAC;CAEtB,OAAO,EAAE,OAAO,MAAM,EAAK,IAAI,CAAC,CAAC;AACnC;AAGA,SAAS,EACP,GACA,GACS;CACT,OACE,EAAgB,EAAM,OAAO,EAAO,KAAK,KACzC,EAAc,EAAM,KAAK,UAAoB,EAAO,KAAK,QAAkB,KAC3E,EAAc,EAAM,KAAK,QAAkB,EAAO,KAAK,MAAgB;AAE3E;AAYA,SAAgB,EACd,GACA,GACgC;CAChC,IAAM,IAA4C,CAAC;CAEnD,KAAK,IAAI,IAAI,GAAG,IAAI,EAAQ,UAAU,EAAU,SAAS,GAAc,KACrE,KAAK,IAAI,IAAI,IAAI,GAAG,IAAI,EAAQ,UAAU,EAAU,SAAS,GAAc,KAAK;EAC9E,IAAM,IAAI,EAAQ,IACZ,IAAI,EAAQ;EAElB,IACE,CAAC,EAAE,KAAK,QACR,CAAC,EAAE,KAAK,QACR,EAAe,EAAE,OAAO,EAAE,KAAK,KAC/B,EAAE,KAAK,aAAa,EAAE,KAAK,YAC3B,EAAE,KAAK,WAAW,EAAE,KAAK,QACzB;GACA,EAAU,KAAK;IACb,QAAQ,EAAE;IACV,QAAQ,EAAE;IACV,MAAM;IACN,OAAO,EAAE;IACT,OAAO,EAAE;GACX,CAAC;GACD;EACF;EAEA,IAAM,IAAgB,EAAe,GAAG,CAAC;EAEzC,AAAI,CAAC,KAAiB,CAAC,EAAE,KAAK,QAAQ,EAAW,GAAG,CAAC,IACnD,EAAU,KAAK;GACb,MAAM;GACN,eAAe,EAAE;GACjB,cAAc,EAAE;GAChB,gBAAgB,EAAE;GAClB,eAAe,EAAE;EACnB,CAAC,IACQ,KAAiB,CAAC,EAAE,KAAK,QAAQ,EAAW,GAAG,CAAC,KACzD,EAAU,KAAK;GACb,MAAM;GACN,eAAe,EAAE;GACjB,cAAc,EAAE;GAChB,gBAAgB,EAAE;GAClB,eAAe,EAAE;EACnB,CAAC;CAEL;CAGF,OAAO;AACT"}
|
package/dist/_dev.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dev.cjs","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __WARD_PROD__?: boolean }).__WARD_PROD__;\n\n/** @internal */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/ward] ${msg}`);\n}\n\n/** @internal */\nexport function error(msg: string, ...args: unknown[]): void {\n if (isDev) console.error(`[@vielzeug/ward] ${msg}`, ...args);\n}\n\n/** @internal — Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() / error() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n"],"mappings":"AAAA,IAAM,EAAQ,CAAE,WAA2C,cAQ3D,SAAgB,EAAM,EAAa,GAAG,EAAuB,CACvD,GAAO,QAAQ,MAAM,oBAAoB,IAAO,GAAG,CAAI,CAC7D"}
|
package/dist/_dev.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dev.d.ts","sourceRoot":"","sources":["../src/_dev.ts"],"names":[],"mappings":""}
|
package/dist/_dev.js
ADDED
package/dist/_dev.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dev.js","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["const isDev = !(globalThis as { __WARD_PROD__?: boolean }).__WARD_PROD__;\n\n/** @internal */\nexport function warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/ward] ${msg}`);\n}\n\n/** @internal */\nexport function error(msg: string, ...args: unknown[]): void {\n if (isDev) console.error(`[@vielzeug/ward] ${msg}`, ...args);\n}\n\n/** @internal — Run fn only in dev builds. Use when dev-only logic goes beyond a single warn() / error() call. */\nexport function devOnly(fn: () => void): void {\n if (isDev) fn();\n}\n"],"mappings":";AAAA,IAAM,IAAQ,CAAE,WAA2C;AAQ3D,SAAgB,EAAM,GAAa,GAAG,GAAuB;CAC3D,AAAI,KAAO,QAAQ,MAAM,oBAAoB,KAAO,GAAG,CAAI;AAC7D"}
|
package/dist/_match.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./constants.cjs"),t=require("./errors.cjs"),n=require("./_dev.cjs"),r=require("./resource.cjs");function i(e){if(typeof e!=`object`||!e)throw new t.WardConfigError(`Invalid principal: expected { id: string, roles: string[] }`);let n=e;if(typeof n.id!=`string`||!n.id.trim())throw new t.WardConfigError(`Invalid principal: id must be a non-empty string`);if(!Array.isArray(n.roles)||n.roles.some(e=>typeof e!=`string`||!e.trim()))throw new t.WardConfigError(`Invalid principal: roles must be an array of non-empty strings`)}function a(e){e!==null&&i(e)}function o(t,n){return n===null?t.includes(e.ANONYMOUS):t.every(e=>e===`anonymous`)?!1:t.includes(`*`)?!0:t.some(e=>n.roles.includes(e))}function s(e,i,a,s,c,l=!1){if(!o(e.roles,i)||!r.matchesPattern(e.rule.resource,a)||s!==void 0&&!r.matchesPattern(e.rule.action,s))return!1;if(l||!e.rule.when)return!0;if(i===null)return!1;try{let t=e.rule.when({data:c,principal:i});return t instanceof Promise&&n.error(`Rule[${e.index}] when() returned a Promise. Async predicates are not supported — the Promise is truthy and will always grant access. Use a sync predicate instead.`),t}catch(n){throw new t.WardPredicateError(e.index,n)}}function c(e,t){return t.priority>e.priority||t.priority===e.priority&&t.score>e.score||t.priority===e.priority&&t.score===e.score&&t.denyBonus>e.denyBonus}function l(e,t,n,r,i){let a;for(let o of e)s(o,t,n,r,i)&&(!a||c(a,o))&&(a=o);return a}function u(e){return e?e.rule.effect===`deny`?{allowed:!1,reason:`explicit-deny`,rule:e.rule}:{allowed:!0,rule:e.rule}:{allowed:!1,reason:`no-matching-rule`}}exports.assertUserPrincipal=i,exports.isOverriddenBy=c,exports.matchesRule=s,exports.pickWinner=l,exports.principalMatchesRoles=o,exports.toDecision=u,exports.validatePrincipal=a;
|
|
2
|
+
//# sourceMappingURL=_match.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_match.cjs","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type { Principal, UserPrincipal, WardDecision, WardRule } from './types';\n\nimport { error } from './_dev';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\n\n// ---------------------------------------------------------------------------\n// Principal validation\n// ---------------------------------------------------------------------------\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const p = input as Record<string, unknown>;\n\n if (typeof p.id !== 'string' || !p.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(p.roles) || p.roles.some((r) => typeof r !== 'string' || !(r as string).trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function validatePrincipal(principal: Principal): void {\n if (principal !== null) {\n assertUserPrincipal(principal);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Role matching\n// ---------------------------------------------------------------------------\n\n/** Returns true if the principal's role set intersects with the rule's roles. */\nexport function principalMatchesRoles(roles: readonly string[], principal: Principal): boolean {\n if (principal === null) {\n return roles.includes(ANONYMOUS);\n }\n\n if (roles.every((r) => r === ANONYMOUS)) return false;\n\n if (roles.includes(WILDCARD)) return true;\n\n return roles.some((role) => principal.roles.includes(role));\n}\n\n// ---------------------------------------------------------------------------\n// Rule matching\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a rule applies to a given request.\n *\n * @param action - Pass `undefined` to skip the action check (used by `rulesInScope`).\n * @param data - Data payload; passed to `when` predicates.\n * @param skipPredicate - When true, omit the `when` evaluation even when data is present.\n */\nexport function matchesRule<TAction extends string, TData>(\n entry: CompiledEntry<TAction, TData>,\n principal: Principal,\n resource: string,\n action: TAction | undefined,\n data: TData | undefined,\n skipPredicate = false,\n): boolean {\n if (!principalMatchesRoles(entry.roles, principal)) return false;\n\n if (!matchesPattern(entry.rule.resource as string, resource)) return false;\n\n if (action !== undefined && !matchesPattern(entry.rule.action as string, action)) return false;\n\n if (skipPredicate || !entry.rule.when) return true;\n\n if (principal === null) return false;\n\n try {\n const result: unknown = entry.rule.when({ data, principal });\n\n if (result instanceof Promise) {\n error(\n `Rule[${entry.index}] when() returned a Promise. Async predicates are not supported — the Promise is truthy and will always grant access. Use a sync predicate instead.`,\n );\n }\n\n return result as boolean;\n } catch (err) {\n throw new WardPredicateError(entry.index, err);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Winner selection\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if `challenger` would displace `current` as the pickWinner result.\n */\nexport function isOverriddenBy<TAction extends string, TData>(\n current: CompiledEntry<TAction, TData>,\n challenger: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n challenger.priority > current.priority ||\n (challenger.priority === current.priority && challenger.score > current.score) ||\n (challenger.priority === current.priority &&\n challenger.score === current.score &&\n challenger.denyBonus > current.denyBonus)\n );\n}\n\nexport function pickWinner<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n): CompiledEntry<TAction, TData> | undefined {\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, action, data)) continue;\n\n if (!winner || isOverriddenBy(winner, entry)) {\n winner = entry;\n }\n }\n\n return winner;\n}\n\nexport function toDecision<TAction extends string, TData>(\n winner: CompiledEntry<TAction, TData> | undefined,\n): WardDecision<TAction, TData> {\n if (!winner) return { allowed: false, reason: 'no-matching-rule' };\n\n if (winner.rule.effect === 'deny') {\n return { allowed: false, reason: 'explicit-deny', rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n }\n\n return { allowed: true, rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n}\n"],"mappings":"iHAaA,SAAgB,EAAoB,EAAgD,CAClF,GAAI,OAAO,GAAU,UAAY,CAAC,EAChC,MAAM,IAAI,EAAA,gBAAgB,6DAA6D,EAGzF,IAAM,EAAI,EAEV,GAAI,OAAO,EAAE,IAAO,UAAY,CAAC,EAAE,GAAG,KAAK,EACzC,MAAM,IAAI,EAAA,gBAAgB,kDAAkD,EAG9E,GAAI,CAAC,MAAM,QAAQ,EAAE,KAAK,GAAK,EAAE,MAAM,KAAM,GAAM,OAAO,GAAM,UAAY,CAAE,EAAa,KAAK,CAAC,EAC/F,MAAM,IAAI,EAAA,gBAAgB,gEAAgE,CAE9F,CAEA,SAAgB,EAAkB,EAA4B,CACxD,IAAc,MAChB,EAAoB,CAAS,CAEjC,CAOA,SAAgB,EAAsB,EAA0B,EAA+B,CAS7F,OARI,IAAc,KACT,EAAM,SAAS,EAAA,SAAS,EAG7B,EAAM,MAAO,GAAM,IAAA,WAAe,EAAU,GAE5C,EAAM,SAAA,GAAiB,EAAU,GAE9B,EAAM,KAAM,GAAS,EAAU,MAAM,SAAS,CAAI,CAAC,CAC5D,CAaA,SAAgB,EACd,EACA,EACA,EACA,EACA,EACA,EAAgB,GACP,CAKT,GAJI,CAAC,EAAsB,EAAM,MAAO,CAAS,GAE7C,CAAC,EAAA,eAAe,EAAM,KAAK,SAAoB,CAAQ,GAEvD,IAAW,IAAA,IAAa,CAAC,EAAA,eAAe,EAAM,KAAK,OAAkB,CAAM,EAAG,MAAO,GAEzF,GAAI,GAAiB,CAAC,EAAM,KAAK,KAAM,MAAO,GAE9C,GAAI,IAAc,KAAM,MAAO,GAE/B,GAAI,CACF,IAAM,EAAkB,EAAM,KAAK,KAAK,CAAE,OAAM,WAAU,CAAC,EAQ3D,OANI,aAAkB,SACpB,EAAA,MACE,QAAQ,EAAM,MAAM,oJACtB,EAGK,CACT,OAAS,EAAK,CACZ,MAAM,IAAI,EAAA,mBAAmB,EAAM,MAAO,CAAG,CAC/C,CACF,CASA,SAAgB,EACd,EACA,EACS,CACT,OACE,EAAW,SAAW,EAAQ,UAC7B,EAAW,WAAa,EAAQ,UAAY,EAAW,MAAQ,EAAQ,OACvE,EAAW,WAAa,EAAQ,UAC/B,EAAW,QAAU,EAAQ,OAC7B,EAAW,UAAY,EAAQ,SAErC,CAEA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAC2C,CAC3C,IAAI,EAEJ,IAAK,IAAM,KAAS,EACb,EAAY,EAAO,EAAW,EAAU,EAAQ,CAAI,IAErD,CAAC,GAAU,EAAe,EAAQ,CAAK,KACzC,EAAS,GAIb,OAAO,CACT,CAEA,SAAgB,EACd,EAC8B,CAO9B,OANK,EAED,EAAO,KAAK,SAAW,OAClB,CAAE,QAAS,GAAO,OAAQ,gBAAiB,KAAM,EAAO,IAA2C,EAGrG,CAAE,QAAS,GAAM,KAAM,EAAO,IAA2C,EAN5D,CAAE,QAAS,GAAO,OAAQ,kBAAmB,CAOnE"}
|
package/dist/_match.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CompiledEntry } from './_compile';
|
|
2
|
+
import type { Principal, UserPrincipal, WardDecision } from './types';
|
|
3
|
+
/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */
|
|
4
|
+
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>;
|
|
22
|
+
//# sourceMappingURL=_match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_match.d.ts","sourceRoot":"","sources":["../src/_match.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAY,MAAM,SAAS,CAAC;AAWhF,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,aAAa,CAclF;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAI5D;AAMD,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAU7F;AAMD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACvD,KAAK,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EACpC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,GAAG,SAAS,EAC3B,IAAI,EAAE,KAAK,GAAG,SAAS,EACvB,aAAa,UAAQ,GACpB,OAAO,CAwBT;AAMD;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EAC1D,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EACtC,UAAU,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,GACxC,OAAO,CAQT;AAED,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACtD,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EACxC,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,IAAI,EAAE,KAAK,GAAG,SAAS,GACtB,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,SAAS,CAY3C;AAED,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACtD,MAAM,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,SAAS,GAChD,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAQ9B"}
|
package/dist/_match.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ANONYMOUS as e } from "./constants.js";
|
|
2
|
+
import { WardConfigError as t, WardPredicateError as n } from "./errors.js";
|
|
3
|
+
import { error as r } from "./_dev.js";
|
|
4
|
+
import { matchesPattern as i } from "./resource.js";
|
|
5
|
+
//#region src/_match.ts
|
|
6
|
+
function a(e) {
|
|
7
|
+
if (typeof e != "object" || !e) throw new t("Invalid principal: expected { id: string, roles: string[] }");
|
|
8
|
+
let n = e;
|
|
9
|
+
if (typeof n.id != "string" || !n.id.trim()) throw new t("Invalid principal: id must be a non-empty string");
|
|
10
|
+
if (!Array.isArray(n.roles) || n.roles.some((e) => typeof e != "string" || !e.trim())) throw new t("Invalid principal: roles must be an array of non-empty strings");
|
|
11
|
+
}
|
|
12
|
+
function o(e) {
|
|
13
|
+
e !== null && a(e);
|
|
14
|
+
}
|
|
15
|
+
function s(t, n) {
|
|
16
|
+
return n === null ? t.includes(e) : t.every((e) => e === "anonymous") ? !1 : t.includes("*") ? !0 : t.some((e) => n.roles.includes(e));
|
|
17
|
+
}
|
|
18
|
+
function c(e, t, a, o, c, l = !1) {
|
|
19
|
+
if (!s(e.roles, t) || !i(e.rule.resource, a) || o !== void 0 && !i(e.rule.action, o)) return !1;
|
|
20
|
+
if (l || !e.rule.when) return !0;
|
|
21
|
+
if (t === null) return !1;
|
|
22
|
+
try {
|
|
23
|
+
let n = e.rule.when({
|
|
24
|
+
data: c,
|
|
25
|
+
principal: t
|
|
26
|
+
});
|
|
27
|
+
return n instanceof Promise && r(`Rule[${e.index}] when() returned a Promise. Async predicates are not supported — the Promise is truthy and will always grant access. Use a sync predicate instead.`), n;
|
|
28
|
+
} catch (t) {
|
|
29
|
+
throw new n(e.index, t);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function l(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;
|
|
34
|
+
}
|
|
35
|
+
function u(e, t, n, r, i) {
|
|
36
|
+
let a;
|
|
37
|
+
for (let o of e) c(o, t, n, r, i) && (!a || l(a, o)) && (a = o);
|
|
38
|
+
return a;
|
|
39
|
+
}
|
|
40
|
+
function d(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
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { a as assertUserPrincipal, l as isOverriddenBy, c as matchesRule, u as pickWinner, s as principalMatchesRoles, d as toDecision, o as validatePrincipal };
|
|
55
|
+
|
|
56
|
+
//# sourceMappingURL=_match.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_match.js","names":[],"sources":["../src/_match.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type { Principal, UserPrincipal, WardDecision, WardRule } from './types';\n\nimport { error } from './_dev';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\n\n// ---------------------------------------------------------------------------\n// Principal validation\n// ---------------------------------------------------------------------------\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const p = input as Record<string, unknown>;\n\n if (typeof p.id !== 'string' || !p.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(p.roles) || p.roles.some((r) => typeof r !== 'string' || !(r as string).trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function validatePrincipal(principal: Principal): void {\n if (principal !== null) {\n assertUserPrincipal(principal);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Role matching\n// ---------------------------------------------------------------------------\n\n/** Returns true if the principal's role set intersects with the rule's roles. */\nexport function principalMatchesRoles(roles: readonly string[], principal: Principal): boolean {\n if (principal === null) {\n return roles.includes(ANONYMOUS);\n }\n\n if (roles.every((r) => r === ANONYMOUS)) return false;\n\n if (roles.includes(WILDCARD)) return true;\n\n return roles.some((role) => principal.roles.includes(role));\n}\n\n// ---------------------------------------------------------------------------\n// Rule matching\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a rule applies to a given request.\n *\n * @param action - Pass `undefined` to skip the action check (used by `rulesInScope`).\n * @param data - Data payload; passed to `when` predicates.\n * @param skipPredicate - When true, omit the `when` evaluation even when data is present.\n */\nexport function matchesRule<TAction extends string, TData>(\n entry: CompiledEntry<TAction, TData>,\n principal: Principal,\n resource: string,\n action: TAction | undefined,\n data: TData | undefined,\n skipPredicate = false,\n): boolean {\n if (!principalMatchesRoles(entry.roles, principal)) return false;\n\n if (!matchesPattern(entry.rule.resource as string, resource)) return false;\n\n if (action !== undefined && !matchesPattern(entry.rule.action as string, action)) return false;\n\n if (skipPredicate || !entry.rule.when) return true;\n\n if (principal === null) return false;\n\n try {\n const result: unknown = entry.rule.when({ data, principal });\n\n if (result instanceof Promise) {\n error(\n `Rule[${entry.index}] when() returned a Promise. Async predicates are not supported — the Promise is truthy and will always grant access. Use a sync predicate instead.`,\n );\n }\n\n return result as boolean;\n } catch (err) {\n throw new WardPredicateError(entry.index, err);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Winner selection\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if `challenger` would displace `current` as the pickWinner result.\n */\nexport function isOverriddenBy<TAction extends string, TData>(\n current: CompiledEntry<TAction, TData>,\n challenger: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n challenger.priority > current.priority ||\n (challenger.priority === current.priority && challenger.score > current.score) ||\n (challenger.priority === current.priority &&\n challenger.score === current.score &&\n challenger.denyBonus > current.denyBonus)\n );\n}\n\nexport function pickWinner<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n): CompiledEntry<TAction, TData> | undefined {\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, action, data)) continue;\n\n if (!winner || isOverriddenBy(winner, entry)) {\n winner = entry;\n }\n }\n\n return winner;\n}\n\nexport function toDecision<TAction extends string, TData>(\n winner: CompiledEntry<TAction, TData> | undefined,\n): WardDecision<TAction, TData> {\n if (!winner) return { allowed: false, reason: 'no-matching-rule' };\n\n if (winner.rule.effect === 'deny') {\n return { allowed: false, reason: 'explicit-deny', rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n }\n\n return { allowed: true, rule: winner.rule as Readonly<WardRule<TAction, TData>> };\n}\n"],"mappings":";;;;;AAaA,SAAgB,EAAoB,GAAgD;CAClF,IAAI,OAAO,KAAU,YAAY,CAAC,GAChC,MAAM,IAAI,EAAgB,6DAA6D;CAGzF,IAAM,IAAI;CAEV,IAAI,OAAO,EAAE,MAAO,YAAY,CAAC,EAAE,GAAG,KAAK,GACzC,MAAM,IAAI,EAAgB,kDAAkD;CAG9E,IAAI,CAAC,MAAM,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,MAAM,MAAM,OAAO,KAAM,YAAY,CAAE,EAAa,KAAK,CAAC,GAC/F,MAAM,IAAI,EAAgB,gEAAgE;AAE9F;AAEA,SAAgB,EAAkB,GAA4B;CAC5D,AAAI,MAAc,QAChB,EAAoB,CAAS;AAEjC;AAOA,SAAgB,EAAsB,GAA0B,GAA+B;CAS7F,OARI,MAAc,OACT,EAAM,SAAS,CAAS,IAG7B,EAAM,OAAO,MAAM,MAAA,WAAe,IAAU,KAE5C,EAAM,SAAA,GAAiB,IAAU,KAE9B,EAAM,MAAM,MAAS,EAAU,MAAM,SAAS,CAAI,CAAC;AAC5D;AAaA,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,IAAgB,IACP;CAKT,IAJI,CAAC,EAAsB,EAAM,OAAO,CAAS,KAE7C,CAAC,EAAe,EAAM,KAAK,UAAoB,CAAQ,KAEvD,MAAW,KAAA,KAAa,CAAC,EAAe,EAAM,KAAK,QAAkB,CAAM,GAAG,OAAO;CAEzF,IAAI,KAAiB,CAAC,EAAM,KAAK,MAAM,OAAO;CAE9C,IAAI,MAAc,MAAM,OAAO;CAE/B,IAAI;EACF,IAAM,IAAkB,EAAM,KAAK,KAAK;GAAE;GAAM;EAAU,CAAC;EAQ3D,OANI,aAAkB,WACpB,EACE,QAAQ,EAAM,MAAM,oJACtB,GAGK;CACT,SAAS,GAAK;EACZ,MAAM,IAAI,EAAmB,EAAM,OAAO,CAAG;CAC/C;AACF;AASA,SAAgB,EACd,GACA,GACS;CACT,OACE,EAAW,WAAW,EAAQ,YAC7B,EAAW,aAAa,EAAQ,YAAY,EAAW,QAAQ,EAAQ,SACvE,EAAW,aAAa,EAAQ,YAC/B,EAAW,UAAU,EAAQ,SAC7B,EAAW,YAAY,EAAQ;AAErC;AAEA,SAAgB,EACd,GACA,GACA,GACA,GACA,GAC2C;CAC3C,IAAI;CAEJ,KAAK,IAAM,KAAS,GACb,EAAY,GAAO,GAAW,GAAU,GAAQ,CAAI,MAErD,CAAC,KAAU,EAAe,GAAQ,CAAK,OACzC,IAAS;CAIb,OAAO;AACT;AAEA,SAAgB,EACd,GAC8B;CAO9B,OANK,IAED,EAAO,KAAK,WAAW,SAClB;EAAE,SAAS;EAAO,QAAQ;EAAiB,MAAM,EAAO;CAA2C,IAGrG;EAAE,SAAS;EAAM,MAAM,EAAO;CAA2C,IAN5D;EAAE,SAAS;EAAO,QAAQ;CAAmB;AAOnE"}
|
package/dist/builder.cjs
ADDED
|
@@ -0,0 +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;
|
|
2
|
+
//# sourceMappingURL=builder.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.cjs","names":[],"sources":["../src/builder.ts"],"sourcesContent":["import type { WardPredicate, WardRule } from './types';\n\nimport { WILDCARD } from './constants';\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// ruleFor — multi-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 | 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 | 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 | 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>(attributeKey: keyof TData & string): 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>(attributeKey: keyof TData & string): WardPredicate<TData> {\n return predicate.owns(attributeKey);\n}\n"],"mappings":"AA4BA,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,KAAsB,EAA0D,CAC9E,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,EAAsB,EAA0D,CAC9F,OAAO,EAAU,KAAK,CAAY,CACpC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { WardPredicate, WardRule } from './types';
|
|
2
|
+
import { WILDCARD } from './constants';
|
|
3
|
+
type RuleOptions<TData> = {
|
|
4
|
+
priority?: number;
|
|
5
|
+
when?: WardPredicate<TData>;
|
|
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 | typeof WILDCARD)[], options?: RuleOptions<TData>): WardRule<TAction, TData>[];
|
|
19
|
+
/**
|
|
20
|
+
* Creates one `WardRule` per action with `effect: 'allow'`.
|
|
21
|
+
*
|
|
22
|
+
* Reads naturally: "allow editor to read/update posts".
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* allow('editor', 'posts', ['read', 'update'])
|
|
27
|
+
* allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function allow<TAction extends string = string, TData = unknown>(role: string | readonly string[], resource: string | typeof WILDCARD, actions: readonly (TAction | typeof WILDCARD)[], options?: RuleOptions<TData>): WardRule<TAction, TData>[];
|
|
31
|
+
/**
|
|
32
|
+
* Creates one `WardRule` per action with `effect: 'deny'`.
|
|
33
|
+
*
|
|
34
|
+
* Reads naturally: "deny blocked from reading posts".
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* deny('blocked', 'posts', ['read', 'update'])
|
|
39
|
+
* deny('guest', WILDCARD, [WILDCARD], { priority: 10 })
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function deny<TAction extends string = string, TData = unknown>(role: string | readonly string[], resource: string | typeof WILDCARD, actions: readonly (TAction | typeof WILDCARD)[], options?: RuleOptions<TData>): WardRule<TAction, TData>[];
|
|
43
|
+
/**
|
|
44
|
+
* Grouped predicate factories. Import as a namespace to avoid name collisions:
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { predicate } from '@vielzeug/ward';
|
|
47
|
+
* allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })
|
|
48
|
+
* allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare const predicate: {
|
|
52
|
+
/**
|
|
53
|
+
* Returns a `WardPredicate` that combines all given predicates with AND semantics —
|
|
54
|
+
* all must return `true` for the rule to match.
|
|
55
|
+
*/
|
|
56
|
+
readonly and: <TData = unknown>(...preds: WardPredicate<TData>[]) => WardPredicate<TData>;
|
|
57
|
+
/**
|
|
58
|
+
* Returns a `WardPredicate` that inverts the given predicate.
|
|
59
|
+
*/
|
|
60
|
+
readonly not: <TData = unknown>(pred: WardPredicate<TData>) => WardPredicate<TData>;
|
|
61
|
+
/**
|
|
62
|
+
* Returns a `WardPredicate` that combines all given predicates with OR semantics —
|
|
63
|
+
* at least one must return `true` for the rule to match.
|
|
64
|
+
*/
|
|
65
|
+
readonly or: <TData = unknown>(...preds: WardPredicate<TData>[]) => WardPredicate<TData>;
|
|
66
|
+
/**
|
|
67
|
+
* Returns a `WardPredicate` that checks whether the data object's `attributeKey` field
|
|
68
|
+
* matches the principal's `id`. Use to express ownership constraints.
|
|
69
|
+
*
|
|
70
|
+
* Must be used with a rule that requires authentication (non-`ANONYMOUS` role).
|
|
71
|
+
* Predicates only execute for authenticated principals — pairing `owns` with an
|
|
72
|
+
* `ANONYMOUS`-role rule produces a rule that can never match because the predicate
|
|
73
|
+
* is skipped for unauthenticated requests.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
readonly owns: <TData = unknown>(attributeKey: keyof TData & string) => WardPredicate<TData>;
|
|
81
|
+
};
|
|
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 & string): WardPredicate<TData>;
|
|
89
|
+
export {};
|
|
90
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEvD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAMvC,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,QAAQ,CAAC,EAAE,EAC/C,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,QAAQ,CAAC,EAAE,EAC/C,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,QAAQ,CAAC,EAAE,EAC/C,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,0BAA0B,MAAM,KAAK,GAAG,MAAM,KAAG,aAAa,CAAC,KAAK,CAAC;CAWvE,CAAC;AAMX;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,YAAY,EAAE,MAAM,KAAK,GAAG,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAE9F"}
|