@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/dist/builder.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/builder.ts
|
|
2
|
+
function e(e, t, n, r, i) {
|
|
3
|
+
return r.map((r) => ({
|
|
4
|
+
action: r,
|
|
5
|
+
effect: e,
|
|
6
|
+
...i?.priority === void 0 ? {} : { priority: i.priority },
|
|
7
|
+
resource: n,
|
|
8
|
+
role: t,
|
|
9
|
+
...i?.when === void 0 ? {} : { when: i.when }
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
function t(t, n, r, i) {
|
|
13
|
+
return e("allow", t, n, r, i);
|
|
14
|
+
}
|
|
15
|
+
function n(t, n, r, i) {
|
|
16
|
+
return e("deny", t, n, r, i);
|
|
17
|
+
}
|
|
18
|
+
var r = {
|
|
19
|
+
and(...e) {
|
|
20
|
+
return (t) => e.every((e) => e(t));
|
|
21
|
+
},
|
|
22
|
+
not(e) {
|
|
23
|
+
return (t) => !e(t);
|
|
24
|
+
},
|
|
25
|
+
or(...e) {
|
|
26
|
+
return (t) => e.some((e) => e(t));
|
|
27
|
+
},
|
|
28
|
+
owns(e) {
|
|
29
|
+
return ({ data: t, principal: n }) => {
|
|
30
|
+
if (!t || typeof t != "object") return !1;
|
|
31
|
+
let r = t;
|
|
32
|
+
return Object.hasOwn(r, e) ? r[e] === n.id : !1;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function i(e) {
|
|
37
|
+
return r.owns(e);
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { t as allow, n as deny, i as owns, r as predicate, e as ruleFor };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.js","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,GACA,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,KAAK,OAAY;EAC9B;EACA;EACA,GAAI,GAAS,aAAa,KAAA,IAA6C,CAAC,IAAlC,EAAE,UAAU,EAAQ,SAAS;EACnE;EACA;EACA,GAAI,GAAS,SAAS,KAAA,IAAqC,CAAC,IAA1B,EAAE,MAAM,EAAQ,KAAK;CACzD,EAAE;AACJ;AAiBA,SAAgB,EACd,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,SAAS,GAAM,GAAU,GAAS,CAAO;AAC1D;AAaA,SAAgB,EACd,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,QAAQ,GAAM,GAAU,GAAS,CAAO;AACzD;AAcA,IAAa,IAAY;CAKvB,IAAqB,GAAG,GAAqD;EAC3E,QAAQ,MAAQ,EAAM,OAAO,MAAM,EAAE,CAAG,CAAC;CAC3C;CAKA,IAAqB,GAAkD;EACrE,QAAQ,MAAQ,CAAC,EAAK,CAAG;CAC3B;CAMA,GAAoB,GAAG,GAAqD;EAC1E,QAAQ,MAAQ,EAAM,MAAM,MAAM,EAAE,CAAG,CAAC;CAC1C;CAgBA,KAAsB,GAA0D;EAC9E,QAAQ,EAAE,SAAM,mBAAgB;GAC9B,IAAI,CAAC,KAAQ,OAAO,KAAS,UAAU,OAAO;GAE9C,IAAM,IAAS;GAIf,OAFK,OAAO,OAAO,GAAQ,CAAY,IAEhC,EAAO,OAAkB,EAAU,KAFO;EAGnD;CACF;AACF;AAYA,SAAgB,EAAsB,GAA0D;CAC9F,OAAO,EAAU,KAAK,CAAY;AACpC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.cjs","names":[],"sources":["../src/constants.ts"],"sourcesContent":["export const WILDCARD = '*' as const;\nexport const ANONYMOUS = 'anonymous' as const;\n"],"mappings":"AAAA,IAAa,EAAW,IACX,EAAY"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,QAAQ,EAAG,GAAY,CAAC;AACrC,eAAO,MAAM,SAAS,EAAG,WAAoB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../src/constants.ts"],"sourcesContent":["export const WILDCARD = '*' as const;\nexport const ANONYMOUS = 'anonymous' as const;\n"],"mappings":";AAAA,IAAa,IAAW,KACX,IAAY"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./factory.cjs");function t(t,n){let r=e=>{let t=e.principal?e.principal.roles.length>0?e.principal.roles.join(`, `):e.principal.id:`anonymous`,n=(e.allowed?`allow`:e.reason).padEnd(16),r=`rule`in e?`(${e.rule.effect})`.padEnd(8):` `;console.debug(`[ward:decision] ${n} ${r} ${t} ${e.resource} ${e.action}`)};return e.createWard(t,{...n,logger:r})}exports.debugWard=t;
|
|
2
|
+
//# sourceMappingURL=devtools.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.cjs","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/ward — debug utilities for authorization decision visualisation.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n * ```\n */\n\nimport type { Ward, WardLoggerContext, WardOptions, WardRule } from './types';\n\nimport { createWard } from './factory';\n\n/**\n * Creates a {@link Ward} with authorization decision logging pre-wired to `console.debug`.\n *\n * Equivalent to `createWard(rules, { ...options, logger: ctx => console.debug(...) })` but\n * imported from a dedicated sub-path so `console.debug` references are tree-shaken from\n * production bundles when this sub-path is not imported.\n *\n * Logs every authorization decision made by any decision method (`checkAll`, `explain`)\n * with `[ward:decision]` prefixes showing the outcome,\n * principal, resource, action, and — when a rule matched — its effect.\n *\n * @example\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n *\n * const permit = debugWard(rules);\n * permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'read');\n * // [ward:decision] allow (allow) viewer posts read\n *\n * permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'delete');\n * // [ward:decision] no-matching-rule viewer posts delete\n *\n * // Note: `trace()` does NOT fire the logger — it is a side-channel-free inspection tool.\n * ```\n */\nexport function debugWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[],\n options?: Omit<WardOptions<TAction, TData>, 'logger'>,\n): Ward<TAction, TData> {\n const logger = (ctx: WardLoggerContext<TAction, TData>): void => {\n const principal = ctx.principal\n ? ctx.principal.roles.length > 0\n ? ctx.principal.roles.join(', ')\n : ctx.principal.id\n : 'anonymous';\n const outcome = ctx.allowed ? 'allow' : ctx.reason;\n const decision = outcome.padEnd(16);\n const effect = 'rule' in ctx ? `(${ctx.rule.effect})`.padEnd(8) : ' ';\n\n console.debug(`[ward:decision] ${decision} ${effect} ${principal} ${ctx.resource} ${ctx.action}`);\n };\n\n return createWard<TAction, TData>(rules, { ...options, logger });\n}\n"],"mappings":"oGAsCA,SAAgB,EACd,EACA,EACsB,CACtB,IAAM,EAAU,GAAiD,CAC/D,IAAM,EAAY,EAAI,UAClB,EAAI,UAAU,MAAM,OAAS,EAC3B,EAAI,UAAU,MAAM,KAAK,IAAI,EAC7B,EAAI,UAAU,GAChB,YAEE,GADU,EAAI,QAAU,QAAU,EAAI,OAAA,CACnB,OAAO,EAAE,EAC5B,EAAS,SAAU,EAAM,IAAI,EAAI,KAAK,OAAO,GAAG,OAAO,CAAC,EAAI,WAElE,QAAQ,MAAM,mBAAmB,EAAS,IAAI,EAAO,IAAI,EAAU,IAAI,EAAI,SAAS,IAAI,EAAI,QAAQ,CACtG,EAEA,OAAO,EAAA,WAA2B,EAAO,CAAE,GAAG,EAAS,QAAO,CAAC,CACjE"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vielzeug/ward — debug utilities for authorization decision visualisation.
|
|
3
|
+
*
|
|
4
|
+
* Import from the dedicated sub-path so it is tree-shaken from production bundles:
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { debugWard } from '@vielzeug/ward/devtools';
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
import type { Ward, WardOptions, WardRule } from './types';
|
|
10
|
+
/**
|
|
11
|
+
* Creates a {@link Ward} with authorization decision logging pre-wired to `console.debug`.
|
|
12
|
+
*
|
|
13
|
+
* Equivalent to `createWard(rules, { ...options, logger: ctx => console.debug(...) })` but
|
|
14
|
+
* imported from a dedicated sub-path so `console.debug` references are tree-shaken from
|
|
15
|
+
* production bundles when this sub-path is not imported.
|
|
16
|
+
*
|
|
17
|
+
* Logs every authorization decision made by any decision method (`checkAll`, `explain`)
|
|
18
|
+
* with `[ward:decision]` prefixes showing the outcome,
|
|
19
|
+
* principal, resource, action, and — when a rule matched — its effect.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { debugWard } from '@vielzeug/ward/devtools';
|
|
24
|
+
*
|
|
25
|
+
* const permit = debugWard(rules);
|
|
26
|
+
* permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'read');
|
|
27
|
+
* // [ward:decision] allow (allow) viewer posts read
|
|
28
|
+
*
|
|
29
|
+
* permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'delete');
|
|
30
|
+
* // [ward:decision] no-matching-rule viewer posts delete
|
|
31
|
+
*
|
|
32
|
+
* // Note: `trace()` does NOT fire the logger — it is a side-channel-free inspection tool.
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function debugWard<TAction extends string = string, TData = unknown>(rules: readonly WardRule<TAction, TData>[], options?: Omit<WardOptions<TAction, TData>, 'logger'>): Ward<TAction, TData>;
|
|
36
|
+
//# sourceMappingURL=devtools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../src/devtools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAqB,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAI9E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACxE,KAAK,EAAE,SAAS,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EAC1C,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,GACpD,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAetB"}
|
package/dist/devtools.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createWard as e } from "./factory.js";
|
|
2
|
+
//#region src/devtools.ts
|
|
3
|
+
function t(t, n) {
|
|
4
|
+
let r = (e) => {
|
|
5
|
+
let t = e.principal ? e.principal.roles.length > 0 ? e.principal.roles.join(", ") : e.principal.id : "anonymous", n = (e.allowed ? "allow" : e.reason).padEnd(16), r = "rule" in e ? `(${e.rule.effect})`.padEnd(8) : " ";
|
|
6
|
+
console.debug(`[ward:decision] ${n} ${r} ${t} ${e.resource} ${e.action}`);
|
|
7
|
+
};
|
|
8
|
+
return e(t, {
|
|
9
|
+
...n,
|
|
10
|
+
logger: r
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { t as debugWard };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=devtools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.js","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/ward — debug utilities for authorization decision visualisation.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n * ```\n */\n\nimport type { Ward, WardLoggerContext, WardOptions, WardRule } from './types';\n\nimport { createWard } from './factory';\n\n/**\n * Creates a {@link Ward} with authorization decision logging pre-wired to `console.debug`.\n *\n * Equivalent to `createWard(rules, { ...options, logger: ctx => console.debug(...) })` but\n * imported from a dedicated sub-path so `console.debug` references are tree-shaken from\n * production bundles when this sub-path is not imported.\n *\n * Logs every authorization decision made by any decision method (`checkAll`, `explain`)\n * with `[ward:decision]` prefixes showing the outcome,\n * principal, resource, action, and — when a rule matched — its effect.\n *\n * @example\n * ```ts\n * import { debugWard } from '@vielzeug/ward/devtools';\n *\n * const permit = debugWard(rules);\n * permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'read');\n * // [ward:decision] allow (allow) viewer posts read\n *\n * permit.explain({ id: 'u1', roles: ['viewer'] }, 'posts', 'delete');\n * // [ward:decision] no-matching-rule viewer posts delete\n *\n * // Note: `trace()` does NOT fire the logger — it is a side-channel-free inspection tool.\n * ```\n */\nexport function debugWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[],\n options?: Omit<WardOptions<TAction, TData>, 'logger'>,\n): Ward<TAction, TData> {\n const logger = (ctx: WardLoggerContext<TAction, TData>): void => {\n const principal = ctx.principal\n ? ctx.principal.roles.length > 0\n ? ctx.principal.roles.join(', ')\n : ctx.principal.id\n : 'anonymous';\n const outcome = ctx.allowed ? 'allow' : ctx.reason;\n const decision = outcome.padEnd(16);\n const effect = 'rule' in ctx ? `(${ctx.rule.effect})`.padEnd(8) : ' ';\n\n console.debug(`[ward:decision] ${decision} ${effect} ${principal} ${ctx.resource} ${ctx.action}`);\n };\n\n return createWard<TAction, TData>(rules, { ...options, logger });\n}\n"],"mappings":";;AAsCA,SAAgB,EACd,GACA,GACsB;CACtB,IAAM,KAAU,MAAiD;EAC/D,IAAM,IAAY,EAAI,YAClB,EAAI,UAAU,MAAM,SAAS,IAC3B,EAAI,UAAU,MAAM,KAAK,IAAI,IAC7B,EAAI,UAAU,KAChB,aAEE,KADU,EAAI,UAAU,UAAU,EAAI,OAAA,CACnB,OAAO,EAAE,GAC5B,IAAS,UAAU,IAAM,IAAI,EAAI,KAAK,OAAO,GAAG,OAAO,CAAC,IAAI;EAElE,QAAQ,MAAM,mBAAmB,EAAS,IAAI,EAAO,IAAI,EAAU,IAAI,EAAI,SAAS,IAAI,EAAI,QAAQ;CACtG;CAEA,OAAO,EAA2B,GAAO;EAAE,GAAG;EAAS;CAAO,CAAC;AACjE"}
|
package/dist/errors.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=class e extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}static is(t){return t instanceof e}},t=class extends e{},n=class extends e{ruleIndex;constructor(e,t){let n=t instanceof Error?t.message:String(t);super(`Rule[${e}] threw: ${n}`,{cause:t}),this.ruleIndex=e}};exports.WardConfigError=t,exports.WardError=e,exports.WardPredicateError=n;
|
|
2
|
+
//# sourceMappingURL=errors.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all ward errors. Use `instanceof WardError` to catch any ward-originated error. */\nexport class WardError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is WardError {\n return err instanceof WardError;\n }\n}\n\n/** Thrown when a rule definition or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/**\n * Thrown when a `when` predicate in a ward rule throws an exception.\n *\n * Catch this to distinguish predicate failures from other errors:\n * ```ts\n * try {\n * ward.explain(principal, resource, action, data);\n * } catch (e) {\n * if (e instanceof WardPredicateError) {\n * console.error(`Predicate in Rule[${e.ruleIndex}] threw`, e.cause);\n * }\n * }\n * ```\n */\nexport class WardPredicateError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n\n super(`Rule[${ruleIndex}] threw: ${msg}`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n"],"mappings":"AACA,IAAa,EAAb,MAAa,UAAkB,KAAM,CACnC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,IAAI,OAAO,KACvB,OAAO,eAAe,KAAM,IAAI,OAAO,SAAS,CAClD,CAEA,OAAO,GAAG,EAAgC,CACxC,OAAO,aAAe,CACxB,CACF,EAGa,EAAb,cAAqC,CAAU,CAAC,EAgBnC,EAAb,cAAwC,CAAU,CAChD,UAEA,YAAY,EAAmB,EAAgB,CAC7C,IAAM,EAAM,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EAEjE,MAAM,QAAQ,EAAU,WAAW,IAAO,CAAE,OAAM,CAAC,EACnD,KAAK,UAAY,CACnB,CACF"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Base class for all ward errors. Use `instanceof WardError` to catch any ward-originated error. */
|
|
2
|
+
export declare class WardError extends Error {
|
|
3
|
+
constructor(message: string, opts?: ErrorOptions);
|
|
4
|
+
static is(err: unknown): err is WardError;
|
|
5
|
+
}
|
|
6
|
+
/** Thrown when a rule definition or principal is malformed. */
|
|
7
|
+
export declare class WardConfigError extends WardError {
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Thrown when a `when` predicate in a ward rule throws an exception.
|
|
11
|
+
*
|
|
12
|
+
* Catch this to distinguish predicate failures from other errors:
|
|
13
|
+
* ```ts
|
|
14
|
+
* try {
|
|
15
|
+
* ward.explain(principal, resource, action, data);
|
|
16
|
+
* } catch (e) {
|
|
17
|
+
* if (e instanceof WardPredicateError) {
|
|
18
|
+
* console.error(`Predicate in Rule[${e.ruleIndex}] threw`, e.cause);
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare class WardPredicateError extends WardError {
|
|
24
|
+
readonly ruleIndex: number;
|
|
25
|
+
constructor(ruleIndex: number, cause: unknown);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,YAAY;IAMhD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,SAAS;CAG1C;AAED,+DAA+D;AAC/D,qBAAa,eAAgB,SAAQ,SAAS;CAAG;AAEjD;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;IAC/C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAM9C"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
var e = class e extends Error {
|
|
3
|
+
constructor(e, t) {
|
|
4
|
+
super(e, t), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype);
|
|
5
|
+
}
|
|
6
|
+
static is(t) {
|
|
7
|
+
return t instanceof e;
|
|
8
|
+
}
|
|
9
|
+
}, t = class extends e {}, n = class extends e {
|
|
10
|
+
ruleIndex;
|
|
11
|
+
constructor(e, t) {
|
|
12
|
+
let n = t instanceof Error ? t.message : String(t);
|
|
13
|
+
super(`Rule[${e}] threw: ${n}`, { cause: t }), this.ruleIndex = e;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { t as WardConfigError, e as WardError, n as WardPredicateError };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all ward errors. Use `instanceof WardError` to catch any ward-originated error. */\nexport class WardError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is WardError {\n return err instanceof WardError;\n }\n}\n\n/** Thrown when a rule definition or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/**\n * Thrown when a `when` predicate in a ward rule throws an exception.\n *\n * Catch this to distinguish predicate failures from other errors:\n * ```ts\n * try {\n * ward.explain(principal, resource, action, data);\n * } catch (e) {\n * if (e instanceof WardPredicateError) {\n * console.error(`Predicate in Rule[${e.ruleIndex}] threw`, e.cause);\n * }\n * }\n * ```\n */\nexport class WardPredicateError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n\n super(`Rule[${ruleIndex}] threw: ${msg}`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n"],"mappings":";AACA,IAAa,IAAb,MAAa,UAAkB,MAAM;CACnC,YAAY,GAAiB,GAAqB;EAGhD,AAFA,MAAM,GAAS,CAAI,GACnB,KAAK,OAAO,IAAI,OAAO,MACvB,OAAO,eAAe,MAAM,IAAI,OAAO,SAAS;CAClD;CAEA,OAAO,GAAG,GAAgC;EACxC,OAAO,aAAe;CACxB;AACF,GAGa,IAAb,cAAqC,EAAU,CAAC,GAgBnC,IAAb,cAAwC,EAAU;CAChD;CAEA,YAAY,GAAmB,GAAgB;EAC7C,IAAM,IAAM,aAAiB,QAAQ,EAAM,UAAU,OAAO,CAAK;EAGjE,AADA,MAAM,QAAQ,EAAU,WAAW,KAAO,EAAE,SAAM,CAAC,GACnD,KAAK,YAAY;CACnB;AACF"}
|
package/dist/factory.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./errors.cjs"),t=require("./_compile.cjs"),n=require("./_match.cjs"),r=require("./_conflict.cjs");function i(e,t,r,i,a){let o=new Set,s=[];for(let c of i)o.has(c)||(o.add(c),n.pickWinner(e,t,r,c,a)?.rule.effect===`allow`&&s.push(c));return s}function a(e,t,r,i){let a=i===void 0,o=[];for(let s of e)n.matchesRule(s,t,r,void 0,i,a)&&o.push(s.rule);return o}function o(o=[],s={}){let{logger:c,maxConflicts:l=1/0}=s,u=o.map((e,n)=>t.compileEntry(e,n));function d(e,t,n,r,i){c&&c({...i,action:n,data:r,principal:e,resource:t})}function f(e,t,r,i){let a=n.toDecision(n.pickWinner(u,e,t,r,i));return d(e,t,r,i,a),a}function p(e,t,r,i){return n.validatePrincipal(e),f(e,t,r,i)}function m(e,t){return t.map(t=>({...f(e,t.resource,t.action,t.data),action:t.action,resource:t.resource}))}function h(e,t){return t.length===0?[]:(n.validatePrincipal(e),m(e,t))}function g(e,t,r,a){return n.validatePrincipal(e),i(u,e,t,r,a)}function _(e,t,r){return n.validatePrincipal(e),a(u,e,t,r)}function v(e,t,r,i){n.validatePrincipal(e);let a=[];for(let o of u)n.matchesRule(o,e,t,r,i)&&a.push(o);let o;for(let e of a)(!o||n.isOverriddenBy(o,e))&&(o=e);let s=n.toDecision(o);return{candidates:a.map(e=>({index:e.index,priority:e.priority,rule:e.rule,score:e.score,won:e===o})),decision:s}}function y(e){n.assertUserPrincipal(e);let t={attributes:e.attributes?structuredClone(e.attributes):void 0,id:e.id,roles:[...e.roles]};return{allowedActions:(e,n,r)=>i(u,t,e,n,r),checkAll:e=>e.length===0?[]:m(t,e),explain:(e,n,r)=>f(t,e,n,r),rulesInScope:(e,n)=>a(u,t,e,n),trace:(e,n,r)=>v(t,e,n,r)}}let b;function x(){return b??=Object.freeze(r.computeConflicts(u,l))}if(s.strict||s.onConflict){let t=x();if(t.length>0&&(s.onConflict&&t.forEach(s.onConflict),s.strict)){let n=t.map(e=>e.kind===`duplicate`?`Rule[${e.indexB}] ${e.kind} of Rule[${e.indexA}]`:`Rule[${e.shadowedIndex}] ${e.kind} by Rule[${e.shadowingIndex}]`).join(`; `);throw new e.WardConfigError(`${t.length} rule conflict(s) detected: ${n}`)}}return{allowedActions:g,checkAll:h,detectConflicts:x,explain:p,forUser:y,rulesInScope:_,trace:v}}exports.createWard=o;
|
|
2
|
+
//# sourceMappingURL=factory.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.cjs","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type {\n BoundWard,\n Principal,\n UserPrincipal,\n Ward,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionResult,\n WardOptions,\n WardRule,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\n\n// ---------------------------------------------------------------------------\n// Shared loop cores (validation-free; used by both public API and forUser)\n// ---------------------------------------------------------------------------\n\nfunction coreAllowedActions<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data: TData | undefined,\n): TAction[] {\n const seen = new Set<TAction>();\n const result: TAction[] = [];\n\n for (const action of knownActions) {\n if (seen.has(action)) continue;\n\n seen.add(action);\n\n const winner = pickWinner(entries, principal, resource, action, data);\n\n if (winner?.rule.effect === 'allow') result.push(action);\n }\n\n return result;\n}\n\nfunction coreRulesInScope<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n data: TData | undefined,\n): CompiledEntry<TAction, TData>['rule'][] {\n const skipPredicate = data === undefined;\n const result: CompiledEntry<TAction, TData>['rule'][] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n const { logger, maxConflicts = Infinity } = options;\n const entries = rules.map((rule, i) => compileEntry(rule, i));\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function fireLogger(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n decision: WardDecision<TAction, TData>,\n ): void {\n if (!logger) return;\n\n logger({ ...decision, action, data, principal, resource } as Parameters<typeof logger>[0]);\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n fireLogger(principal, resource, action, data, decision);\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n\n function explain(\n principal: Principal,\n resource: string,\n action: TAction,\n data?: TData,\n ): WardDecision<TAction, TData> {\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data?: TData,\n ): TAction[] {\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(\n principal: Principal,\n resource: string,\n data?: TData,\n ): ReadonlyArray<Readonly<WardRule<TAction, TData>>> {\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(principal: Principal, resource: string, action: TAction, data?: TData): WardTrace<TAction, TData> {\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (resource, knownActions, data?) =>\n coreAllowedActions(entries, snap, resource, knownActions, data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (resource, action, data?) => evaluateAndLog(snap, resource, action, data),\n rulesInScope: (resource, data?) => coreRulesInScope(entries, snap, resource, data),\n trace: (resource, action, data?) => trace(snap, resource, action, data),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, trace };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Ward, WardOptions, WardRule } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Creates an authorization ward from a set of rules.
|
|
4
|
+
*
|
|
5
|
+
* **Winner selection** when multiple rules match a request:
|
|
6
|
+
* 1. Higher `priority` wins.
|
|
7
|
+
* 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,
|
|
8
|
+
* applied independently to role, resource, and action).
|
|
9
|
+
* 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).
|
|
10
|
+
* 4. On absolute tie (identical priority, specificity, and effect), the rule declared
|
|
11
|
+
* **first in the input array** wins.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createWard<TAction extends string = string, TData = unknown>(rules?: readonly WardRule<TAction, TData>[], options?: WardOptions<TAction, TData>): Ward<TAction, TData>;
|
|
14
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,IAAI,EAKJ,WAAW,EACX,QAAQ,EAGT,MAAM,SAAS,CAAC;AAwDjB;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACzE,KAAK,GAAE,SAAS,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAO,EAC/C,OAAO,GAAE,WAAW,CAAC,OAAO,EAAE,KAAK,CAAM,GACxC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CA4KtB"}
|
package/dist/factory.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { WardConfigError as e } from "./errors.js";
|
|
2
|
+
import { compileEntry as t } from "./_compile.js";
|
|
3
|
+
import { assertUserPrincipal as n, isOverriddenBy as r, matchesRule as i, pickWinner as a, toDecision as o, validatePrincipal as s } from "./_match.js";
|
|
4
|
+
import { computeConflicts as c } from "./_conflict.js";
|
|
5
|
+
//#region src/factory.ts
|
|
6
|
+
function l(e, t, n, r, i) {
|
|
7
|
+
let o = /* @__PURE__ */ new Set(), s = [];
|
|
8
|
+
for (let c of r) o.has(c) || (o.add(c), a(e, t, n, c, i)?.rule.effect === "allow" && s.push(c));
|
|
9
|
+
return s;
|
|
10
|
+
}
|
|
11
|
+
function u(e, t, n, r) {
|
|
12
|
+
let a = r === void 0, o = [];
|
|
13
|
+
for (let s of e) i(s, t, n, void 0, r, a) && o.push(s.rule);
|
|
14
|
+
return o;
|
|
15
|
+
}
|
|
16
|
+
function d(d = [], f = {}) {
|
|
17
|
+
let { logger: p, maxConflicts: m = Infinity } = f, h = d.map((e, n) => t(e, n));
|
|
18
|
+
function g(e, t, n, r, i) {
|
|
19
|
+
p && p({
|
|
20
|
+
...i,
|
|
21
|
+
action: n,
|
|
22
|
+
data: r,
|
|
23
|
+
principal: e,
|
|
24
|
+
resource: t
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function _(e, t, n, r) {
|
|
28
|
+
let i = o(a(h, e, t, n, r));
|
|
29
|
+
return g(e, t, n, r, i), i;
|
|
30
|
+
}
|
|
31
|
+
function v(e, t, n, r) {
|
|
32
|
+
return s(e), _(e, t, n, r);
|
|
33
|
+
}
|
|
34
|
+
function y(e, t) {
|
|
35
|
+
return t.map((t) => ({
|
|
36
|
+
..._(e, t.resource, t.action, t.data),
|
|
37
|
+
action: t.action,
|
|
38
|
+
resource: t.resource
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
function b(e, t) {
|
|
42
|
+
return t.length === 0 ? [] : (s(e), y(e, t));
|
|
43
|
+
}
|
|
44
|
+
function x(e, t, n, r) {
|
|
45
|
+
return s(e), l(h, e, t, n, r);
|
|
46
|
+
}
|
|
47
|
+
function S(e, t, n) {
|
|
48
|
+
return s(e), u(h, e, t, n);
|
|
49
|
+
}
|
|
50
|
+
function C(e, t, n, a) {
|
|
51
|
+
s(e);
|
|
52
|
+
let c = [];
|
|
53
|
+
for (let r of h) i(r, e, t, n, a) && c.push(r);
|
|
54
|
+
let l;
|
|
55
|
+
for (let e of c) (!l || r(l, e)) && (l = e);
|
|
56
|
+
let u = o(l);
|
|
57
|
+
return {
|
|
58
|
+
candidates: c.map((e) => ({
|
|
59
|
+
index: e.index,
|
|
60
|
+
priority: e.priority,
|
|
61
|
+
rule: e.rule,
|
|
62
|
+
score: e.score,
|
|
63
|
+
won: e === l
|
|
64
|
+
})),
|
|
65
|
+
decision: u
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function w(e) {
|
|
69
|
+
n(e);
|
|
70
|
+
let t = {
|
|
71
|
+
attributes: e.attributes ? structuredClone(e.attributes) : void 0,
|
|
72
|
+
id: e.id,
|
|
73
|
+
roles: [...e.roles]
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
allowedActions: (e, n, r) => l(h, t, e, n, r),
|
|
77
|
+
checkAll: (e) => e.length === 0 ? [] : y(t, e),
|
|
78
|
+
explain: (e, n, r) => _(t, e, n, r),
|
|
79
|
+
rulesInScope: (e, n) => u(h, t, e, n),
|
|
80
|
+
trace: (e, n, r) => C(t, e, n, r)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
let T;
|
|
84
|
+
function E() {
|
|
85
|
+
return T ??= Object.freeze(c(h, m));
|
|
86
|
+
}
|
|
87
|
+
if (f.strict || f.onConflict) {
|
|
88
|
+
let t = E();
|
|
89
|
+
if (t.length > 0 && (f.onConflict && t.forEach(f.onConflict), f.strict)) {
|
|
90
|
+
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("; ");
|
|
91
|
+
throw new e(`${t.length} rule conflict(s) detected: ${n}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
allowedActions: x,
|
|
96
|
+
checkAll: b,
|
|
97
|
+
detectConflicts: E,
|
|
98
|
+
explain: v,
|
|
99
|
+
forUser: w,
|
|
100
|
+
rulesInScope: S,
|
|
101
|
+
trace: C
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { d as createWard };
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +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":";;;;;AAyBA,SAAS,EACP,GACA,GACA,GACA,GACA,GACW;CACX,IAAM,oBAAO,IAAI,IAAa,GACxB,IAAoB,CAAC;CAE3B,KAAK,IAAM,KAAU,GACf,EAAK,IAAI,CAAM,MAEnB,EAAK,IAAI,CAAM,GAEA,EAAW,GAAS,GAAW,GAAU,GAAQ,CAE5D,CAAA,EAAQ,KAAK,WAAW,WAAS,EAAO,KAAK,CAAM;CAGzD,OAAO;AACT;AAEA,SAAS,EACP,GACA,GACA,GACA,GACyC;CACzC,IAAM,IAAgB,MAAS,KAAA,GACzB,IAAkD,CAAC;CAEzD,KAAK,IAAM,KAAS,GACb,EAAY,GAAO,GAAW,GAAU,KAAA,GAAW,GAAM,CAAa,KAE3E,EAAO,KAAK,EAAM,IAAI;CAGxB,OAAO;AACT;AAiBA,SAAgB,EACd,IAA6C,CAAC,GAC9C,IAAuC,CAAC,GAClB;CACtB,IAAM,EAAE,WAAQ,kBAAe,aAAa,GACtC,IAAU,EAAM,KAAK,GAAM,MAAM,EAAa,GAAM,CAAC,CAAC;CAM5D,SAAS,EACP,GACA,GACA,GACA,GACA,GACM;EACD,KAEL,EAAO;GAAE,GAAG;GAAU;GAAQ;GAAM;GAAW;EAAS,CAAiC;CAC3F;CAEA,SAAS,EACP,GACA,GACA,GACA,GAC8B;EAE9B,IAAM,IAAW,EADF,EAAW,GAAS,GAAW,GAAU,GAAQ,CACpC,CAAM;EAIlC,OAFA,EAAW,GAAW,GAAU,GAAQ,GAAM,CAAQ,GAE/C;CACT;CAMA,SAAS,EACP,GACA,GACA,GACA,GAC8B;EAG9B,OAFA,EAAkB,CAAS,GAEpB,EAAe,GAAW,GAAU,GAAQ,CAAI;CACzD;CAEA,SAAS,EACP,GACA,GACsC;EACtC,OAAO,EAAO,KAAK,OAAW;GAC5B,GAAG,EAAe,GAAW,EAAM,UAAU,EAAM,QAAQ,EAAM,IAAI;GACrE,QAAQ,EAAM;GACd,UAAU,EAAM;EAClB,EAAE;CACJ;CAEA,SAAS,EACP,GACA,GACsC;EAKtC,OAJI,EAAO,WAAW,IAAU,CAAC,KAEjC,EAAkB,CAAS,GAEpB,EAAY,GAAW,CAAM;CACtC;CAEA,SAAS,EACP,GACA,GACA,GACA,GACW;EAGX,OAFA,EAAkB,CAAS,GAEpB,EAAmB,GAAS,GAAW,GAAU,GAAc,CAAI;CAC5E;CAEA,SAAS,EACP,GACA,GACA,GACmD;EAGnD,OAFA,EAAkB,CAAS,GAEpB,EAAiB,GAAS,GAAW,GAAU,CAAI;CAC5D;CAEA,SAAS,EAAM,GAAsB,GAAkB,GAAiB,GAAyC;EAC/G,EAAkB,CAAS;EAE3B,IAAM,IAA4C,CAAC;EAEnD,KAAK,IAAM,KAAS,GAClB,AAAI,EAAY,GAAO,GAAW,GAAU,GAAQ,CAAI,KACtD,EAAS,KAAK,CAAK;EAIvB,IAAI;EAEJ,KAAK,IAAM,KAAS,GAClB,CAAI,CAAC,KAAU,EAAe,GAAQ,CAAK,OAAG,IAAS;EAGzD,IAAM,IAAW,EAAW,CAAM;EAUlC,OAAO;GAAE,YARgD,EAAS,KAAK,OAAW;IAChF,OAAO,EAAM;IACb,UAAU,EAAM;IAChB,MAAM,EAAM;IACZ,OAAO,EAAM;IACb,KAAK,MAAU;GACjB,EAES;GAAY;EAAS;CAChC;CAEA,SAAS,EAAQ,GAAqD;EACpE,EAAoB,CAAS;EAE7B,IAAM,IAAsB;GAC1B,YAAY,EAAU,aAAa,gBAAgB,EAAU,UAAU,IAAI,KAAA;GAC3E,IAAI,EAAU;GACd,OAAO,CAAC,GAAG,EAAU,KAAK;EAC5B;EAEA,OAAO;GACL,iBAAiB,GAAU,GAAc,MACvC,EAAmB,GAAS,GAAM,GAAU,GAAc,CAAI;GAChE,WAAW,MAAY,EAAO,WAAW,IAAI,CAAC,IAAI,EAAY,GAAM,CAAM;GAC1E,UAAU,GAAU,GAAQ,MAAU,EAAe,GAAM,GAAU,GAAQ,CAAI;GACjF,eAAe,GAAU,MAAU,EAAiB,GAAS,GAAM,GAAU,CAAI;GACjF,QAAQ,GAAU,GAAQ,MAAU,EAAM,GAAM,GAAU,GAAQ,CAAI;EACxE;CACF;CAMA,IAAI;CAEJ,SAAS,IAA2D;EAClE,OAAQ,MAAmB,OAAO,OAAO,EAAiB,GAAS,CAAY,CAAC;CAClF;CAEA,IAAI,EAAQ,UAAU,EAAQ,YAAY;EACxC,IAAM,IAAY,EAAgB;EAElC,IAAI,EAAU,SAAS,MACjB,EAAQ,cAAY,EAAU,QAAQ,EAAQ,UAAU,GAExD,EAAQ,SAAQ;GAClB,IAAM,IAAU,EACb,KAAK,MACJ,EAAE,SAAS,cACP,QAAQ,EAAE,OAAO,IAAI,EAAE,KAAK,WAAW,EAAE,OAAO,KAChD,QAAQ,EAAE,cAAc,IAAI,EAAE,KAAK,WAAW,EAAE,eAAe,EACrE,CAAC,CACA,KAAK,IAAI;GAEZ,MAAM,IAAI,EAAgB,GAAG,EAAU,OAAO,8BAA8B,GAAS;EACvF;CAEJ;CAEA,OAAO;EAAE;EAAgB;EAAU;EAAiB;EAAS;EAAS;EAAc;CAAM;AAC5F"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./constants.cjs"),t=require("./errors.cjs"),n=require("./resource.cjs"),r=require("./factory.cjs"),i=require("./builder.cjs"),a=require("./middleware.cjs");exports.ANONYMOUS=e.ANONYMOUS,exports.WILDCARD=e.WILDCARD,exports.WardConfigError=t.WardConfigError,exports.WardError=t.WardError,exports.WardPredicateError=t.WardPredicateError,exports.allow=i.allow,exports.createWard=r.createWard,exports.deny=i.deny,exports.guardRequest=a.guardRequest,exports.guardRequestWith=a.guardRequestWith,exports.matchesPattern=n.matchesPattern,exports.owns=i.owns,exports.patternCovers=n.patternCovers,exports.predicate=i.predicate,exports.ruleFor=i.ruleFor;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { ANONYMOUS, WILDCARD } from './constants';
|
|
2
|
+
export { allow, deny, owns, predicate, ruleFor } from './builder';
|
|
3
|
+
export { WardConfigError, WardError, WardPredicateError } from './errors';
|
|
4
|
+
export { createWard } from './factory';
|
|
5
|
+
export { guardRequest, guardRequestWith } from './middleware';
|
|
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';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3D,YAAY,EACV,SAAS,EACT,YAAY,EACZ,SAAS,EACT,WAAW,EACX,aAAa,EACb,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,QAAQ,EACR,SAAS,EACT,kBAAkB,GACnB,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ANONYMOUS as e, WILDCARD as t } from "./constants.js";
|
|
2
|
+
import { WardConfigError as n, WardError as r, WardPredicateError as i } from "./errors.js";
|
|
3
|
+
import { matchesPattern as a, patternCovers as o } from "./resource.js";
|
|
4
|
+
import { createWard as s } from "./factory.js";
|
|
5
|
+
import { allow as c, deny as l, owns as u, predicate as d, ruleFor as f } from "./builder.js";
|
|
6
|
+
import { guardRequest as p, guardRequestWith as m } from "./middleware.js";
|
|
7
|
+
export { e as ANONYMOUS, t as WILDCARD, n as WardConfigError, r as WardError, i as WardPredicateError, c as allow, s as createWard, l as deny, p as guardRequest, m as guardRequestWith, a as matchesPattern, u as owns, o as patternCovers, d as predicate, f as ruleFor };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e,t,n,r,i){let a=e.explain(t,n,r,i);return a.allowed?{granted:!0,principal:t}:{decision:a,granted:!1,principal:t,reason:a.reason}}async function t(t,n,r,i,a,o){return e(t,await r(n),i,a,o)}exports.guardRequest=e,exports.guardRequestWith=t;
|
|
2
|
+
//# sourceMappingURL=middleware.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.cjs","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["import type { Principal, Ward, WardDecision } from './types';\n\nexport type WardRequest = Record<string, unknown>;\nexport type PrincipalExtractor<TReq extends WardRequest = WardRequest> = (req: TReq) => Principal | Promise<Principal>;\n\nexport type GuardResult<TAction extends string, TData> =\n | { granted: true; principal: Principal }\n | {\n decision: WardDecision<TAction, TData>;\n granted: false;\n principal: Principal;\n reason: 'explicit-deny' | 'no-matching-rule';\n };\n\n/**\n * Evaluates a ward decision for a known `principal` and returns a `GuardResult`.\n * Use when you have already resolved the principal (e.g. from a session or JWT).\n *\n * For request-object based flows where the principal must be extracted asynchronously,\n * use `guardRequestWith` instead.\n *\n * @example\n * ```ts\n * // Express / Hono / any framework — write a 3-line adapter:\n * app.use('/posts', async (req, res, next) => {\n * const result = await guardRequestWith(ward, req, extractPrincipal, 'posts', 'read');\n * result.granted ? next() : res.status(403).json({ reason: result.reason });\n * });\n * ```\n */\nexport function guardRequest<TAction extends string, TData>(\n ward: Ward<TAction, TData>,\n principal: Principal,\n resource: string,\n action: TAction,\n data?: TData,\n): GuardResult<TAction, TData> {\n const decision = ward.explain(principal, resource, action, data);\n\n if (decision.allowed) {\n return { granted: true, principal };\n }\n\n return { decision, granted: false, principal, reason: decision.reason };\n}\n\n/**\n * Evaluates a ward decision by first extracting the principal from a request object,\n * then running the authorization check. The extractor may be async (e.g. to verify a JWT).\n *\n * Use `guardRequest` instead when the principal is already resolved.\n */\nexport async function guardRequestWith<TAction extends string, TData, TReq extends WardRequest>(\n ward: Ward<TAction, TData>,\n req: TReq,\n extractPrincipal: PrincipalExtractor<TReq>,\n resource: string,\n action: TAction,\n data?: TData,\n): Promise<GuardResult<TAction, TData>> {\n const principal = await extractPrincipal(req);\n\n return guardRequest(ward, principal, resource, action, data);\n}\n"],"mappings":"AA8BA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAC6B,CAC7B,IAAM,EAAW,EAAK,QAAQ,EAAW,EAAU,EAAQ,CAAI,EAM/D,OAJI,EAAS,QACJ,CAAE,QAAS,GAAM,WAAU,EAG7B,CAAE,WAAU,QAAS,GAAO,YAAW,OAAQ,EAAS,MAAO,CACxE,CAQA,eAAsB,EACpB,EACA,EACA,EACA,EACA,EACA,EACsC,CAGtC,OAAO,EAAa,EAAM,MAFF,EAAiB,CAAG,EAEP,EAAU,EAAQ,CAAI,CAC7D"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Principal, Ward, WardDecision } from './types';
|
|
2
|
+
export type WardRequest = Record<string, unknown>;
|
|
3
|
+
export type PrincipalExtractor<TReq extends WardRequest = WardRequest> = (req: TReq) => Principal | Promise<Principal>;
|
|
4
|
+
export type GuardResult<TAction extends string, TData> = {
|
|
5
|
+
granted: true;
|
|
6
|
+
principal: Principal;
|
|
7
|
+
} | {
|
|
8
|
+
decision: WardDecision<TAction, TData>;
|
|
9
|
+
granted: false;
|
|
10
|
+
principal: Principal;
|
|
11
|
+
reason: 'explicit-deny' | 'no-matching-rule';
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Evaluates a ward decision for a known `principal` and returns a `GuardResult`.
|
|
15
|
+
* Use when you have already resolved the principal (e.g. from a session or JWT).
|
|
16
|
+
*
|
|
17
|
+
* For request-object based flows where the principal must be extracted asynchronously,
|
|
18
|
+
* use `guardRequestWith` instead.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // Express / Hono / any framework — write a 3-line adapter:
|
|
23
|
+
* app.use('/posts', async (req, res, next) => {
|
|
24
|
+
* const result = await guardRequestWith(ward, req, extractPrincipal, 'posts', 'read');
|
|
25
|
+
* result.granted ? next() : res.status(403).json({ reason: result.reason });
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare function guardRequest<TAction extends string, TData>(ward: Ward<TAction, TData>, principal: Principal, resource: string, action: TAction, data?: TData): GuardResult<TAction, TData>;
|
|
30
|
+
/**
|
|
31
|
+
* Evaluates a ward decision by first extracting the principal from a request object,
|
|
32
|
+
* then running the authorization check. The extractor may be async (e.g. to verify a JWT).
|
|
33
|
+
*
|
|
34
|
+
* Use `guardRequest` instead when the principal is already resolved.
|
|
35
|
+
*/
|
|
36
|
+
export declare function guardRequestWith<TAction extends string, TData, TReq extends WardRequest>(ward: Ward<TAction, TData>, req: TReq, extractPrincipal: PrincipalExtractor<TReq>, resource: string, action: TAction, data?: TData): Promise<GuardResult<TAction, TData>>;
|
|
37
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE7D,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAClD,MAAM,MAAM,kBAAkB,CAAC,IAAI,SAAS,WAAW,GAAG,WAAW,IAAI,CAAC,GAAG,EAAE,IAAI,KAAK,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;AAEvH,MAAM,MAAM,WAAW,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,IACjD;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,GACvC;IACE,QAAQ,EAAE,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACvC,OAAO,EAAE,KAAK,CAAC;IACf,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,eAAe,GAAG,kBAAkB,CAAC;CAC9C,CAAC;AAEN;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACxD,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,EAC1B,SAAS,EAAE,SAAS,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,IAAI,CAAC,EAAE,KAAK,GACX,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,CAQ7B;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EAAE,IAAI,SAAS,WAAW,EAC5F,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,EAC1B,GAAG,EAAE,IAAI,EACT,gBAAgB,EAAE,kBAAkB,CAAC,IAAI,CAAC,EAC1C,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,IAAI,CAAC,EAAE,KAAK,GACX,OAAO,CAAC,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAItC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/middleware.ts
|
|
2
|
+
function e(e, t, n, r, i) {
|
|
3
|
+
let a = e.explain(t, n, r, i);
|
|
4
|
+
return a.allowed ? {
|
|
5
|
+
granted: !0,
|
|
6
|
+
principal: t
|
|
7
|
+
} : {
|
|
8
|
+
decision: a,
|
|
9
|
+
granted: !1,
|
|
10
|
+
principal: t,
|
|
11
|
+
reason: a.reason
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async function t(t, n, r, i, a, o) {
|
|
15
|
+
return e(t, await r(n), i, a, o);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { e as guardRequest, t as guardRequestWith };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["import type { Principal, Ward, WardDecision } from './types';\n\nexport type WardRequest = Record<string, unknown>;\nexport type PrincipalExtractor<TReq extends WardRequest = WardRequest> = (req: TReq) => Principal | Promise<Principal>;\n\nexport type GuardResult<TAction extends string, TData> =\n | { granted: true; principal: Principal }\n | {\n decision: WardDecision<TAction, TData>;\n granted: false;\n principal: Principal;\n reason: 'explicit-deny' | 'no-matching-rule';\n };\n\n/**\n * Evaluates a ward decision for a known `principal` and returns a `GuardResult`.\n * Use when you have already resolved the principal (e.g. from a session or JWT).\n *\n * For request-object based flows where the principal must be extracted asynchronously,\n * use `guardRequestWith` instead.\n *\n * @example\n * ```ts\n * // Express / Hono / any framework — write a 3-line adapter:\n * app.use('/posts', async (req, res, next) => {\n * const result = await guardRequestWith(ward, req, extractPrincipal, 'posts', 'read');\n * result.granted ? next() : res.status(403).json({ reason: result.reason });\n * });\n * ```\n */\nexport function guardRequest<TAction extends string, TData>(\n ward: Ward<TAction, TData>,\n principal: Principal,\n resource: string,\n action: TAction,\n data?: TData,\n): GuardResult<TAction, TData> {\n const decision = ward.explain(principal, resource, action, data);\n\n if (decision.allowed) {\n return { granted: true, principal };\n }\n\n return { decision, granted: false, principal, reason: decision.reason };\n}\n\n/**\n * Evaluates a ward decision by first extracting the principal from a request object,\n * then running the authorization check. The extractor may be async (e.g. to verify a JWT).\n *\n * Use `guardRequest` instead when the principal is already resolved.\n */\nexport async function guardRequestWith<TAction extends string, TData, TReq extends WardRequest>(\n ward: Ward<TAction, TData>,\n req: TReq,\n extractPrincipal: PrincipalExtractor<TReq>,\n resource: string,\n action: TAction,\n data?: TData,\n): Promise<GuardResult<TAction, TData>> {\n const principal = await extractPrincipal(req);\n\n return guardRequest(ward, principal, resource, action, data);\n}\n"],"mappings":";AA8BA,SAAgB,EACd,GACA,GACA,GACA,GACA,GAC6B;CAC7B,IAAM,IAAW,EAAK,QAAQ,GAAW,GAAU,GAAQ,CAAI;CAM/D,OAJI,EAAS,UACJ;EAAE,SAAS;EAAM;CAAU,IAG7B;EAAE;EAAU,SAAS;EAAO;EAAW,QAAQ,EAAS;CAAO;AACxE;AAQA,eAAsB,EACpB,GACA,GACA,GACA,GACA,GACA,GACsC;CAGtC,OAAO,EAAa,GAAM,MAFF,EAAiB,CAAG,GAEP,GAAU,GAAQ,CAAI;AAC7D"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
require("./constants.cjs");function e(e,t){if(e===`*`||e===t)return!0;if(e.endsWith(`:*`)){let n=e.slice(0,-1);return t.startsWith(n)}return!1}function t(e,t){return e===`*`?!0:t===`*`?!1:e.endsWith(`:*`)?t.startsWith(e.slice(0,-1)):e===t}exports.matchesPattern=e,exports.patternCovers=t;
|
|
2
|
+
//# sourceMappingURL=resource.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.cjs","names":[],"sources":["../src/resource.ts"],"sourcesContent":["import { WILDCARD } from './constants';\n\n/**\n * Matches a rule pattern against a concrete value string.\n * Works for both resource patterns and action patterns (action hierarchy).\n *\n * Matching rules (evaluated in order):\n * 1. `WILDCARD` (`*`) matches any value string.\n * 2. An exact string matches itself.\n * 3. A colon-namespaced pattern ending in `:*` (e.g. `posts:*` or `read:*`)\n * matches any string that starts with the given namespace prefix followed by `:`.\n * Example: `posts:*` matches `posts:123`, `posts:draft:42`, etc.\n * `read:*` matches `read:own`, `read:all`, etc.\n *\n * @example\n * matchesPattern('*', 'posts:123') // true — global wildcard\n * matchesPattern('posts', 'posts') // true — exact\n * matchesPattern('posts:*', 'posts:123') // true — namespace wildcard\n * matchesPattern('posts:*', 'comments:1') // false — different namespace\n * matchesPattern('posts:42', 'posts:42') // true — exact namespaced id\n * matchesPattern('read:*', 'read:own') // true — action namespace wildcard\n * matchesPattern('read:*', 'write:all') // false — different action namespace\n */\nexport function matchesPattern(pattern: string, value: string): boolean {\n if (pattern === WILDCARD) return true;\n\n if (pattern === value) return true;\n\n // Namespace wildcard: \"namespace:*\" matches \"namespace:anything\"\n if (pattern.endsWith(':*')) {\n const ns = pattern.slice(0, -1); // keep the trailing colon as prefix delimiter\n\n return value.startsWith(ns);\n }\n\n return false;\n}\n\n/**\n * Returns true if every concrete value that matches `narrow` also matches `broad`.\n * This is the coverage relation used for rule conflict detection.\n *\n * Pattern hierarchy (most to least permissive): `*` > `ns:*` > exact\n *\n * Unlike `matchesPattern(broad, narrow)` — which works accidentally — this\n * function explicitly documents and tests the coverage semantics so it remains\n * correct if new pattern syntax is added in the future.\n *\n * @example\n * patternCovers('*', 'posts:*') // true — * covers everything\n * patternCovers('posts:*', 'posts:123') // true — namespace covers exact\n * patternCovers('posts:*', 'posts:*') // true — same pattern covers itself\n * patternCovers('posts:*', '*') // false — namespace does not cover global wildcard\n * patternCovers('posts', 'posts:*') // false — exact does not cover namespace\n */\nexport function patternCovers(broad: string, narrow: string): boolean {\n // Global wildcard covers everything\n if (broad === WILDCARD) return true;\n\n // Nothing else covers global wildcard\n if (narrow === WILDCARD) return false;\n\n // Namespace wildcard: covers any value starting with the same namespace prefix,\n // including nested namespace wildcards (\"posts:*\" covers \"posts:sub:*\") and exact IDs.\n if (broad.endsWith(':*')) {\n return narrow.startsWith(broad.slice(0, -1));\n }\n\n // Exact only covers itself\n return broad === narrow;\n}\n"],"mappings":"2BAuBA,SAAgB,EAAe,EAAiB,EAAwB,CAGtE,GAFI,IAAA,KAEA,IAAY,EAAO,MAAO,GAG9B,GAAI,EAAQ,SAAS,IAAI,EAAG,CAC1B,IAAM,EAAK,EAAQ,MAAM,EAAG,EAAE,EAE9B,OAAO,EAAM,WAAW,CAAE,CAC5B,CAEA,MAAO,EACT,CAmBA,SAAgB,EAAc,EAAe,EAAyB,CAcpE,OAZI,IAAA,IAA2B,GAG3B,IAAA,IAA4B,GAI5B,EAAM,SAAS,IAAI,EACd,EAAO,WAAW,EAAM,MAAM,EAAG,EAAE,CAAC,EAItC,IAAU,CACnB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matches a rule pattern against a concrete value string.
|
|
3
|
+
* Works for both resource patterns and action patterns (action hierarchy).
|
|
4
|
+
*
|
|
5
|
+
* Matching rules (evaluated in order):
|
|
6
|
+
* 1. `WILDCARD` (`*`) matches any value string.
|
|
7
|
+
* 2. An exact string matches itself.
|
|
8
|
+
* 3. A colon-namespaced pattern ending in `:*` (e.g. `posts:*` or `read:*`)
|
|
9
|
+
* matches any string that starts with the given namespace prefix followed by `:`.
|
|
10
|
+
* Example: `posts:*` matches `posts:123`, `posts:draft:42`, etc.
|
|
11
|
+
* `read:*` matches `read:own`, `read:all`, etc.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* matchesPattern('*', 'posts:123') // true — global wildcard
|
|
15
|
+
* matchesPattern('posts', 'posts') // true — exact
|
|
16
|
+
* matchesPattern('posts:*', 'posts:123') // true — namespace wildcard
|
|
17
|
+
* matchesPattern('posts:*', 'comments:1') // false — different namespace
|
|
18
|
+
* matchesPattern('posts:42', 'posts:42') // true — exact namespaced id
|
|
19
|
+
* matchesPattern('read:*', 'read:own') // true — action namespace wildcard
|
|
20
|
+
* matchesPattern('read:*', 'write:all') // false — different action namespace
|
|
21
|
+
*/
|
|
22
|
+
export declare function matchesPattern(pattern: string, value: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Returns true if every concrete value that matches `narrow` also matches `broad`.
|
|
25
|
+
* This is the coverage relation used for rule conflict detection.
|
|
26
|
+
*
|
|
27
|
+
* Pattern hierarchy (most to least permissive): `*` > `ns:*` > exact
|
|
28
|
+
*
|
|
29
|
+
* Unlike `matchesPattern(broad, narrow)` — which works accidentally — this
|
|
30
|
+
* function explicitly documents and tests the coverage semantics so it remains
|
|
31
|
+
* correct if new pattern syntax is added in the future.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* patternCovers('*', 'posts:*') // true — * covers everything
|
|
35
|
+
* patternCovers('posts:*', 'posts:123') // true — namespace covers exact
|
|
36
|
+
* patternCovers('posts:*', 'posts:*') // true — same pattern covers itself
|
|
37
|
+
* patternCovers('posts:*', '*') // false — namespace does not cover global wildcard
|
|
38
|
+
* patternCovers('posts', 'posts:*') // false — exact does not cover namespace
|
|
39
|
+
*/
|
|
40
|
+
export declare function patternCovers(broad: string, narrow: string): boolean;
|
|
41
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAatE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAepE"}
|