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