@vielzeug/ward 26.10.0 → 26.10.5
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 +1 -1
- package/dist/_compile.cjs +1 -1
- package/dist/_compile.cjs.map +1 -1
- package/dist/_compile.js +1 -1
- package/dist/_compile.js.map +1 -1
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.ts +1 -1
- package/dist/factory.js.map +1 -1
- package/dist/resource.cjs.map +1 -1
- package/dist/resource.d.ts +7 -7
- package/dist/resource.js.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 +2 -2
package/README.md
CHANGED
|
@@ -51,4 +51,4 @@ if (decision.effect === 'allow') {
|
|
|
51
51
|
|
|
52
52
|
## License
|
|
53
53
|
|
|
54
|
-
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu)
|
|
54
|
+
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu): part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
|
package/dist/_compile.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./errors.cjs");function snapshotValue(t,n,r){if(t===null||typeof t==`boolean`||typeof t==`string`)return t;if(typeof t==`number`){if(!Number.isFinite(t))throw new e.WardConfigError(`${n} must contain only finite numbers`);return t}if(typeof t!=`object`)throw new e.WardConfigError(`${n} must contain only JSON-compatible values`);if(r.has(t))throw new e.WardConfigError(`${n} must not contain circular references`);let i=Object.getPrototypeOf(t);if(!Array.isArray(t)&&i!==Object.prototype&&i!==null)throw new e.WardConfigError(`${n} must contain only arrays and plain objects`);r.add(t);try{return Object.freeze(Array.isArray(t)?Array.from(t,(e,t)=>snapshotValue(e,`${n}[${t}]`,r)):Object.fromEntries(Object.keys(t).map(e=>[e,snapshotValue(t[e],`${n}.${e}`,r)])))}finally{r.delete(t)}}function snapshotAttributes(t,n){if(t!==void 0){if(typeof t!=`object`||!t||Array.isArray(t))throw new e.WardConfigError(`${n} must be a plain object`);return snapshotValue(t,n,new Set)}}function compileRule(t,n){let r=`Rule[${n}]`;if(typeof t!=`object`||!t||Array.isArray(t))throw new e.WardConfigError(`${r} must be a plain object`);let i=Object.getPrototypeOf(t);if(i!==Object.prototype&&i!==null)throw new e.WardConfigError(`${r} must be a plain object`);if(typeof t.action!=`string`||!t.action.trim())throw new e.WardConfigError(`${r}.action must be a non-empty string`);if(t.action.endsWith(`:`))throw new e.WardConfigError(`${r}.action '${t.action}' ends with ':'
|
|
1
|
+
const e=require("./errors.cjs");function snapshotValue(t,n,r){if(t===null||typeof t==`boolean`||typeof t==`string`)return t;if(typeof t==`number`){if(!Number.isFinite(t))throw new e.WardConfigError(`${n} must contain only finite numbers`);return t}if(typeof t!=`object`)throw new e.WardConfigError(`${n} must contain only JSON-compatible values`);if(r.has(t))throw new e.WardConfigError(`${n} must not contain circular references`);let i=Object.getPrototypeOf(t);if(!Array.isArray(t)&&i!==Object.prototype&&i!==null)throw new e.WardConfigError(`${n} must contain only arrays and plain objects`);r.add(t);try{return Object.freeze(Array.isArray(t)?Array.from(t,(e,t)=>snapshotValue(e,`${n}[${t}]`,r)):Object.fromEntries(Object.keys(t).map(e=>[e,snapshotValue(t[e],`${n}.${e}`,r)])))}finally{r.delete(t)}}function snapshotAttributes(t,n){if(t!==void 0){if(typeof t!=`object`||!t||Array.isArray(t))throw new e.WardConfigError(`${n} must be a plain object`);return snapshotValue(t,n,new Set)}}function compileRule(t,n){let r=`Rule[${n}]`;if(typeof t!=`object`||!t||Array.isArray(t))throw new e.WardConfigError(`${r} must be a plain object`);let i=Object.getPrototypeOf(t);if(i!==Object.prototype&&i!==null)throw new e.WardConfigError(`${r} must be a plain object`);if(typeof t.action!=`string`||!t.action.trim())throw new e.WardConfigError(`${r}.action must be a non-empty string`);if(t.action.endsWith(`:`))throw new e.WardConfigError(`${r}.action '${t.action}' ends with ':': did you mean '${t.action}*'?`);if(typeof t.resource!=`string`||!t.resource.trim())throw new e.WardConfigError(`${r}.resource must be a non-empty string`);if(t.resource.endsWith(`:`))throw new e.WardConfigError(`${r}.resource '${t.resource}' ends with ':': did you mean '${t.resource}*'?`);if(t.effect!==`allow`&&t.effect!==`deny`)throw new e.WardConfigError(`${r}.effect must be "allow" or "deny"`);if(t.condition!==void 0&&typeof t.condition!=`function`)throw new e.WardConfigError(`${r}.condition must be a function`);if(t.roles!==void 0&&(!Array.isArray(t.roles)||t.roles.some(e=>typeof e!=`string`||!e.trim())))throw new e.WardConfigError(`${r}.roles must be an array of non-empty strings`);return Object.freeze({action:t.action,...t.attributes===void 0?{}:{attributes:snapshotAttributes(t.attributes,`${r}.attributes`)},...t.condition===void 0?{}:{condition:t.condition},effect:t.effect,resource:t.resource,...t.roles===void 0?{}:{roles:Object.freeze([...t.roles])}})}exports.compileRule=compileRule,exports.snapshotAttributes=snapshotAttributes;
|
|
2
2
|
//# sourceMappingURL=_compile.cjs.map
|
package/dist/_compile.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_compile.cjs","names":[],"sources":["../src/_compile.ts"],"sourcesContent":["import { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':'
|
|
1
|
+
{"version":3,"file":"_compile.cjs","names":[],"sources":["../src/_compile.ts"],"sourcesContent":["import { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':': did you mean '${rule.action}*'?`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if (rule.resource.endsWith(':')) {\n throw new WardConfigError(`${at}.resource '${rule.resource}' ends with ':': did you mean '${rule.resource}*'?`);\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.condition !== undefined && typeof rule.condition !== 'function') {\n throw new WardConfigError(`${at}.condition must be a function`);\n }\n\n if (rule.roles !== undefined) {\n if (!Array.isArray(rule.roles) || rule.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError(`${at}.roles must be an array of non-empty strings`);\n }\n }\n\n return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n ...(rule.roles === undefined ? {} : { roles: Object.freeze([...rule.roles]) }),\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n"],"mappings":"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"}
|
package/dist/_compile.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{WardConfigError as e}from"./errors.js";function snapshotValue(t,n,r){if(t===null||typeof t==`boolean`||typeof t==`string`)return t;if(typeof t==`number`){if(!Number.isFinite(t))throw new e(`${n} must contain only finite numbers`);return t}if(typeof t!=`object`)throw new e(`${n} must contain only JSON-compatible values`);if(r.has(t))throw new e(`${n} must not contain circular references`);let i=Object.getPrototypeOf(t);if(!Array.isArray(t)&&i!==Object.prototype&&i!==null)throw new e(`${n} must contain only arrays and plain objects`);r.add(t);try{return Object.freeze(Array.isArray(t)?Array.from(t,(e,t)=>snapshotValue(e,`${n}[${t}]`,r)):Object.fromEntries(Object.keys(t).map(e=>[e,snapshotValue(t[e],`${n}.${e}`,r)])))}finally{r.delete(t)}}function snapshotAttributes(t,n){if(t!==void 0){if(typeof t!=`object`||!t||Array.isArray(t))throw new e(`${n} must be a plain object`);return snapshotValue(t,n,new Set)}}function compileRule(t,n){let r=`Rule[${n}]`;if(typeof t!=`object`||!t||Array.isArray(t))throw new e(`${r} must be a plain object`);let i=Object.getPrototypeOf(t);if(i!==Object.prototype&&i!==null)throw new e(`${r} must be a plain object`);if(typeof t.action!=`string`||!t.action.trim())throw new e(`${r}.action must be a non-empty string`);if(t.action.endsWith(`:`))throw new e(`${r}.action '${t.action}' ends with ':'
|
|
1
|
+
import{WardConfigError as e}from"./errors.js";function snapshotValue(t,n,r){if(t===null||typeof t==`boolean`||typeof t==`string`)return t;if(typeof t==`number`){if(!Number.isFinite(t))throw new e(`${n} must contain only finite numbers`);return t}if(typeof t!=`object`)throw new e(`${n} must contain only JSON-compatible values`);if(r.has(t))throw new e(`${n} must not contain circular references`);let i=Object.getPrototypeOf(t);if(!Array.isArray(t)&&i!==Object.prototype&&i!==null)throw new e(`${n} must contain only arrays and plain objects`);r.add(t);try{return Object.freeze(Array.isArray(t)?Array.from(t,(e,t)=>snapshotValue(e,`${n}[${t}]`,r)):Object.fromEntries(Object.keys(t).map(e=>[e,snapshotValue(t[e],`${n}.${e}`,r)])))}finally{r.delete(t)}}function snapshotAttributes(t,n){if(t!==void 0){if(typeof t!=`object`||!t||Array.isArray(t))throw new e(`${n} must be a plain object`);return snapshotValue(t,n,new Set)}}function compileRule(t,n){let r=`Rule[${n}]`;if(typeof t!=`object`||!t||Array.isArray(t))throw new e(`${r} must be a plain object`);let i=Object.getPrototypeOf(t);if(i!==Object.prototype&&i!==null)throw new e(`${r} must be a plain object`);if(typeof t.action!=`string`||!t.action.trim())throw new e(`${r}.action must be a non-empty string`);if(t.action.endsWith(`:`))throw new e(`${r}.action '${t.action}' ends with ':': did you mean '${t.action}*'?`);if(typeof t.resource!=`string`||!t.resource.trim())throw new e(`${r}.resource must be a non-empty string`);if(t.resource.endsWith(`:`))throw new e(`${r}.resource '${t.resource}' ends with ':': did you mean '${t.resource}*'?`);if(t.effect!==`allow`&&t.effect!==`deny`)throw new e(`${r}.effect must be "allow" or "deny"`);if(t.condition!==void 0&&typeof t.condition!=`function`)throw new e(`${r}.condition must be a function`);if(t.roles!==void 0&&(!Array.isArray(t.roles)||t.roles.some(e=>typeof e!=`string`||!e.trim())))throw new e(`${r}.roles must be an array of non-empty strings`);return Object.freeze({action:t.action,...t.attributes===void 0?{}:{attributes:snapshotAttributes(t.attributes,`${r}.attributes`)},...t.condition===void 0?{}:{condition:t.condition},effect:t.effect,resource:t.resource,...t.roles===void 0?{}:{roles:Object.freeze([...t.roles])}})}export{compileRule,snapshotAttributes};
|
|
2
2
|
//# sourceMappingURL=_compile.js.map
|
package/dist/_compile.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_compile.js","names":[],"sources":["../src/_compile.ts"],"sourcesContent":["import { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':'
|
|
1
|
+
{"version":3,"file":"_compile.js","names":[],"sources":["../src/_compile.ts"],"sourcesContent":["import { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':': did you mean '${rule.action}*'?`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if (rule.resource.endsWith(':')) {\n throw new WardConfigError(`${at}.resource '${rule.resource}' ends with ':': did you mean '${rule.resource}*'?`);\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.condition !== undefined && typeof rule.condition !== 'function') {\n throw new WardConfigError(`${at}.condition must be a function`);\n }\n\n if (rule.roles !== undefined) {\n if (!Array.isArray(rule.roles) || rule.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError(`${at}.roles must be an array of non-empty strings`);\n }\n }\n\n return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n ...(rule.roles === undefined ? {} : { roles: Object.freeze([...rule.roles]) }),\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n"],"mappings":"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"}
|
package/dist/factory.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.cjs","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import { tapper } from '@vielzeug/arsenal';\n\nimport { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport type {\n BoundWard,\n Principal,\n Ward,\n WardAttributes,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardRule,\n} from './types';\n\n/**\n * Creates an authorization ward from an ordered list of rules.\n *\n * **Decision model**: rules are evaluated in declaration order; the first rule\n * that matches the request wins. If no rule matches, the decision is **deny**\n * (default deny). There is no priority, specificity scoring, or conflict\n * detection
|
|
1
|
+
{"version":3,"file":"factory.cjs","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import { tapper } from '@vielzeug/arsenal';\n\nimport { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport type {\n BoundWard,\n Principal,\n Ward,\n WardAttributes,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardRule,\n} from './types';\n\n/**\n * Creates an authorization ward from an ordered list of rules.\n *\n * **Decision model**: rules are evaluated in declaration order; the first rule\n * that matches the request wins. If no rule matches, the decision is **deny**\n * (default deny). There is no priority, specificity scoring, or conflict\n * detection: order is the only tiebreaker.\n *\n * A rule matches when:\n * 1. Its `action` pattern matches the requested action (`*` or `ns:*` or exact).\n * 2. Its `resource` pattern matches the requested resource.\n * 3. Its declarative `roles` (if any) include the principal's role, `ANONYMOUS`\n * matches a null principal, and `WILDCARD` matches any authenticated principal.\n * 4. Every key in its declarative `attributes` is present and equal in the\n * request's `attributes` (deep equality).\n * 5. Its `condition` escape-hatch callback (if any) returns `true`.\n */\nexport function createWard<\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n rules: readonly (\n | WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>\n | readonly WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>[]\n )[] = [],\n): Ward<TAction, TResource, TAttributes> {\n const compiled = Object.freeze(rules.flat().map((rule, index) => compileRule(rule, index)));\n const knownActions = Object.freeze([\n ...new Set(\n compiled.flatMap((rule) => (rule.effect === 'allow' && !rule.action.includes('*') ? [rule.action] : [])),\n ),\n ]) as readonly TAction[];\n const tappers = tapper<WardEvent<TAction, TResource, TAttributes>>();\n\n function evaluate(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n emit: boolean,\n ): WardDecision<TAction, TResource, TAttributes> {\n const normalized = snapshotDecisionInput(input);\n\n for (let index = 0; index < compiled.length; index++) {\n const rule = compiled[index];\n\n if (ruleMatches(rule, index, normalized)) {\n const decision = Object.freeze({\n effect: rule.effect,\n matched: true,\n reason: `rule[${index}] ${rule.effect === 'allow' ? 'allows' : 'denies'} '${normalized.action}' on '${normalized.resource}'`,\n rule,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n }\n\n const decision = Object.freeze({\n effect: 'deny',\n matched: false,\n reason: `no matching rule for '${normalized.action}' on '${normalized.resource}' (default deny)`,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n\n function emitDecision(\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n decision: WardDecision<TAction, TResource, TAttributes>,\n ): void {\n tappers.emit(Object.freeze({ decision, input, type: 'decision' as const }));\n }\n\n function decide(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n ): WardDecision<TAction, TResource, TAttributes> {\n return evaluate(input, true);\n }\n\n function checkAll(\n inputs: readonly WardDecisionInput<TAction, TResource, TAttributes>[],\n ): WardDecision<TAction, TResource, TAttributes>[] {\n return inputs.map(decide);\n }\n\n function allowedActions(input: {\n attributes?: TAttributes;\n filter?: readonly TAction[];\n principal?: Principal;\n resource: TResource;\n }): TAction[] {\n const candidates = input.filter ?? knownActions;\n const seen = new Set<TAction>();\n\n return candidates.filter((action) => {\n if (seen.has(action)) return false;\n seen.add(action);\n return evaluate({ ...input, action }, false).effect === 'allow';\n });\n }\n\n function forPrincipal(principal?: Principal): BoundWard<TAction, TResource, TAttributes> {\n const snapshot = snapshotPrincipal(principal);\n\n return {\n allowedActions: (input) => allowedActions({ ...input, principal: snapshot }),\n checkAll: (inputs) => checkAll(inputs.map((input) => ({ ...input, principal: snapshot }))),\n decide: (input) => decide({ ...input, principal: snapshot }),\n };\n }\n\n return {\n allowedActions,\n checkAll,\n decide,\n forPrincipal,\n knownActions,\n rules: compiled,\n tap: (handler, options) => tappers.tap(handler, options),\n };\n}\n"],"mappings":"+FAgCA,SAAgB,WAKd,EAGM,CAAC,EACgC,CACvC,IAAM,EAAW,OAAO,OAAO,EAAM,KAAK,CAAC,CAAC,KAAK,EAAM,IAAU,EAAA,YAAY,EAAM,CAAK,CAAC,CAAC,EACpF,EAAe,OAAO,OAAO,CACjC,GAAG,IAAI,IACL,EAAS,QAAS,GAAU,EAAK,SAAW,SAAW,CAAC,EAAK,OAAO,SAAS,GAAG,EAAI,CAAC,EAAK,MAAM,EAAI,CAAC,CAAE,CACzG,CACF,CAAC,EACK,GAAA,EAAU,EAAA,OAAA,CAAmD,EAEnE,SAAS,SACP,EACA,EAC+C,CAC/C,IAAM,EAAa,EAAA,sBAAsB,CAAK,EAE9C,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAS,OAAQ,IAAS,CACpD,IAAM,EAAO,EAAS,GAEtB,GAAI,EAAA,YAAY,EAAM,EAAO,CAAU,EAAG,CACxC,IAAM,EAAW,OAAO,OAAO,CAC7B,OAAQ,EAAK,OACb,QAAS,GACT,OAAQ,QAAQ,EAAM,IAAI,EAAK,SAAW,QAAU,SAAW,SAAS,IAAI,EAAW,OAAO,QAAQ,EAAW,SAAS,GAC1H,MACF,CAAC,EAGD,OADI,GAAM,aAAa,EAAY,CAAQ,EACpC,CACT,CACF,CAEA,IAAM,EAAW,OAAO,OAAO,CAC7B,OAAQ,OACR,QAAS,GACT,OAAQ,yBAAyB,EAAW,OAAO,QAAQ,EAAW,SAAS,iBACjF,CAAC,EAGD,OADI,GAAM,aAAa,EAAY,CAAQ,EACpC,CACT,CAEA,SAAS,aACP,EACA,EACM,CACN,EAAQ,KAAK,OAAO,OAAO,CAAE,WAAU,QAAO,KAAM,UAAoB,CAAC,CAAC,CAC5E,CAEA,SAAS,OACP,EAC+C,CAC/C,OAAO,SAAS,EAAO,EAAI,CAC7B,CAEA,SAAS,SACP,EACiD,CACjD,OAAO,EAAO,IAAI,MAAM,CAC1B,CAEA,SAAS,eAAe,EAKV,CACZ,IAAM,EAAa,EAAM,QAAU,EAC7B,EAAO,IAAI,IAEjB,OAAO,EAAW,OAAQ,GACxB,CAAI,EAAK,IAAI,CAAM,IACnB,EAAK,IAAI,CAAM,EACR,SAAS,CAAE,GAAG,EAAO,QAAO,EAAG,EAAK,CAAC,CAAC,SAAW,QACzD,CACH,CAEA,SAAS,aAAa,EAAmE,CACvF,IAAM,EAAW,EAAA,kBAAkB,CAAS,EAE5C,MAAO,CACL,eAAiB,GAAU,eAAe,CAAE,GAAG,EAAO,UAAW,CAAS,CAAC,EAC3E,SAAW,GAAW,SAAS,EAAO,IAAK,IAAW,CAAE,GAAG,EAAO,UAAW,CAAS,EAAE,CAAC,EACzF,OAAS,GAAU,OAAO,CAAE,GAAG,EAAO,UAAW,CAAS,CAAC,CAC7D,CACF,CAEA,MAAO,CACL,eACA,SACA,OACA,aACA,eACA,MAAO,EACP,KAAM,EAAS,IAAY,EAAQ,IAAI,EAAS,CAAO,CACzD,CACF"}
|
package/dist/factory.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Ward, WardAttributes, WardRule } from './types';
|
|
|
5
5
|
* **Decision model**: rules are evaluated in declaration order; the first rule
|
|
6
6
|
* that matches the request wins. If no rule matches, the decision is **deny**
|
|
7
7
|
* (default deny). There is no priority, specificity scoring, or conflict
|
|
8
|
-
* detection
|
|
8
|
+
* detection: order is the only tiebreaker.
|
|
9
9
|
*
|
|
10
10
|
* A rule matches when:
|
|
11
11
|
* 1. Its `action` pattern matches the requested action (`*` or `ns:*` or exact).
|
package/dist/factory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.js","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import { tapper } from '@vielzeug/arsenal';\n\nimport { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport type {\n BoundWard,\n Principal,\n Ward,\n WardAttributes,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardRule,\n} from './types';\n\n/**\n * Creates an authorization ward from an ordered list of rules.\n *\n * **Decision model**: rules are evaluated in declaration order; the first rule\n * that matches the request wins. If no rule matches, the decision is **deny**\n * (default deny). There is no priority, specificity scoring, or conflict\n * detection
|
|
1
|
+
{"version":3,"file":"factory.js","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import { tapper } from '@vielzeug/arsenal';\n\nimport { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport type {\n BoundWard,\n Principal,\n Ward,\n WardAttributes,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardRule,\n} from './types';\n\n/**\n * Creates an authorization ward from an ordered list of rules.\n *\n * **Decision model**: rules are evaluated in declaration order; the first rule\n * that matches the request wins. If no rule matches, the decision is **deny**\n * (default deny). There is no priority, specificity scoring, or conflict\n * detection: order is the only tiebreaker.\n *\n * A rule matches when:\n * 1. Its `action` pattern matches the requested action (`*` or `ns:*` or exact).\n * 2. Its `resource` pattern matches the requested resource.\n * 3. Its declarative `roles` (if any) include the principal's role, `ANONYMOUS`\n * matches a null principal, and `WILDCARD` matches any authenticated principal.\n * 4. Every key in its declarative `attributes` is present and equal in the\n * request's `attributes` (deep equality).\n * 5. Its `condition` escape-hatch callback (if any) returns `true`.\n */\nexport function createWard<\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n rules: readonly (\n | WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>\n | readonly WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>[]\n )[] = [],\n): Ward<TAction, TResource, TAttributes> {\n const compiled = Object.freeze(rules.flat().map((rule, index) => compileRule(rule, index)));\n const knownActions = Object.freeze([\n ...new Set(\n compiled.flatMap((rule) => (rule.effect === 'allow' && !rule.action.includes('*') ? [rule.action] : [])),\n ),\n ]) as readonly TAction[];\n const tappers = tapper<WardEvent<TAction, TResource, TAttributes>>();\n\n function evaluate(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n emit: boolean,\n ): WardDecision<TAction, TResource, TAttributes> {\n const normalized = snapshotDecisionInput(input);\n\n for (let index = 0; index < compiled.length; index++) {\n const rule = compiled[index];\n\n if (ruleMatches(rule, index, normalized)) {\n const decision = Object.freeze({\n effect: rule.effect,\n matched: true,\n reason: `rule[${index}] ${rule.effect === 'allow' ? 'allows' : 'denies'} '${normalized.action}' on '${normalized.resource}'`,\n rule,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n }\n\n const decision = Object.freeze({\n effect: 'deny',\n matched: false,\n reason: `no matching rule for '${normalized.action}' on '${normalized.resource}' (default deny)`,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n\n function emitDecision(\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n decision: WardDecision<TAction, TResource, TAttributes>,\n ): void {\n tappers.emit(Object.freeze({ decision, input, type: 'decision' as const }));\n }\n\n function decide(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n ): WardDecision<TAction, TResource, TAttributes> {\n return evaluate(input, true);\n }\n\n function checkAll(\n inputs: readonly WardDecisionInput<TAction, TResource, TAttributes>[],\n ): WardDecision<TAction, TResource, TAttributes>[] {\n return inputs.map(decide);\n }\n\n function allowedActions(input: {\n attributes?: TAttributes;\n filter?: readonly TAction[];\n principal?: Principal;\n resource: TResource;\n }): TAction[] {\n const candidates = input.filter ?? knownActions;\n const seen = new Set<TAction>();\n\n return candidates.filter((action) => {\n if (seen.has(action)) return false;\n seen.add(action);\n return evaluate({ ...input, action }, false).effect === 'allow';\n });\n }\n\n function forPrincipal(principal?: Principal): BoundWard<TAction, TResource, TAttributes> {\n const snapshot = snapshotPrincipal(principal);\n\n return {\n allowedActions: (input) => allowedActions({ ...input, principal: snapshot }),\n checkAll: (inputs) => checkAll(inputs.map((input) => ({ ...input, principal: snapshot }))),\n decide: (input) => decide({ ...input, principal: snapshot }),\n };\n }\n\n return {\n allowedActions,\n checkAll,\n decide,\n forPrincipal,\n knownActions,\n rules: compiled,\n tap: (handler, options) => tappers.tap(handler, options),\n };\n}\n"],"mappings":"mLAgCA,SAAgB,WAKd,EAGM,CAAC,EACgC,CACvC,IAAM,EAAW,OAAO,OAAO,EAAM,KAAK,CAAC,CAAC,KAAK,EAAM,IAAU,EAAY,EAAM,CAAK,CAAC,CAAC,EACpF,EAAe,OAAO,OAAO,CACjC,GAAG,IAAI,IACL,EAAS,QAAS,GAAU,EAAK,SAAW,SAAW,CAAC,EAAK,OAAO,SAAS,GAAG,EAAI,CAAC,EAAK,MAAM,EAAI,CAAC,CAAE,CACzG,CACF,CAAC,EACK,EAAU,EAAmD,EAEnE,SAAS,SACP,EACA,EAC+C,CAC/C,IAAM,EAAa,EAAsB,CAAK,EAE9C,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAS,OAAQ,IAAS,CACpD,IAAM,EAAO,EAAS,GAEtB,GAAI,EAAY,EAAM,EAAO,CAAU,EAAG,CACxC,IAAM,EAAW,OAAO,OAAO,CAC7B,OAAQ,EAAK,OACb,QAAS,GACT,OAAQ,QAAQ,EAAM,IAAI,EAAK,SAAW,QAAU,SAAW,SAAS,IAAI,EAAW,OAAO,QAAQ,EAAW,SAAS,GAC1H,MACF,CAAC,EAGD,OADI,GAAM,aAAa,EAAY,CAAQ,EACpC,CACT,CACF,CAEA,IAAM,EAAW,OAAO,OAAO,CAC7B,OAAQ,OACR,QAAS,GACT,OAAQ,yBAAyB,EAAW,OAAO,QAAQ,EAAW,SAAS,iBACjF,CAAC,EAGD,OADI,GAAM,aAAa,EAAY,CAAQ,EACpC,CACT,CAEA,SAAS,aACP,EACA,EACM,CACN,EAAQ,KAAK,OAAO,OAAO,CAAE,WAAU,QAAO,KAAM,UAAoB,CAAC,CAAC,CAC5E,CAEA,SAAS,OACP,EAC+C,CAC/C,OAAO,SAAS,EAAO,EAAI,CAC7B,CAEA,SAAS,SACP,EACiD,CACjD,OAAO,EAAO,IAAI,MAAM,CAC1B,CAEA,SAAS,eAAe,EAKV,CACZ,IAAM,EAAa,EAAM,QAAU,EAC7B,EAAO,IAAI,IAEjB,OAAO,EAAW,OAAQ,GACxB,CAAI,EAAK,IAAI,CAAM,IACnB,EAAK,IAAI,CAAM,EACR,SAAS,CAAE,GAAG,EAAO,QAAO,EAAG,EAAK,CAAC,CAAC,SAAW,QACzD,CACH,CAEA,SAAS,aAAa,EAAmE,CACvF,IAAM,EAAW,EAAkB,CAAS,EAE5C,MAAO,CACL,eAAiB,GAAU,eAAe,CAAE,GAAG,EAAO,UAAW,CAAS,CAAC,EAC3E,SAAW,GAAW,SAAS,EAAO,IAAK,IAAW,CAAE,GAAG,EAAO,UAAW,CAAS,EAAE,CAAC,EACzF,OAAS,GAAU,OAAO,CAAE,GAAG,EAAO,UAAW,CAAS,CAAC,CAC7D,CACF,CAEA,MAAO,CACL,eACA,SACA,OACA,aACA,eACA,MAAO,EACP,KAAM,EAAS,IAAY,EAAQ,IAAI,EAAS,CAAO,CACzD,CACF"}
|
package/dist/resource.cjs.map
CHANGED
|
@@ -1 +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
|
|
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"],"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"}
|
package/dist/resource.d.ts
CHANGED
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
* `read:*` matches `read:own`, `read:all`, etc.
|
|
12
12
|
*
|
|
13
13
|
* @example
|
|
14
|
-
* matchesPattern('*', 'posts:123') // true
|
|
15
|
-
* matchesPattern('posts', 'posts') // true
|
|
16
|
-
* matchesPattern('posts:*', 'posts:123') // true
|
|
17
|
-
* matchesPattern('posts:*', 'comments:1') // false
|
|
18
|
-
* matchesPattern('posts:42', 'posts:42') // true
|
|
19
|
-
* matchesPattern('read:*', 'read:own') // true
|
|
20
|
-
* matchesPattern('read:*', 'write:all') // false
|
|
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
21
|
*/
|
|
22
22
|
export declare function matchesPattern(pattern: string, value: string): boolean;
|
|
23
23
|
//# sourceMappingURL=resource.d.ts.map
|
package/dist/resource.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.js","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
|
|
1
|
+
{"version":3,"file":"resource.js","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"],"mappings":"uBAuBA,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"}
|
package/dist/ward.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@vielzeug/arsenal");var t=`*`,n=`anonymous`,WardError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},WardConfigError=class extends WardError{},WardConditionError=class extends WardError{ruleIndex;constructor(e,t){super(`Rule[${e}].condition failed`,{cause:t}),this.ruleIndex=e}};function snapshotValue(e,t,n){if(e===null||typeof e==`boolean`||typeof e==`string`)return e;if(typeof e==`number`){if(!Number.isFinite(e))throw new WardConfigError(`${t} must contain only finite numbers`);return e}if(typeof e!=`object`)throw new WardConfigError(`${t} must contain only JSON-compatible values`);if(n.has(e))throw new WardConfigError(`${t} must not contain circular references`);let r=Object.getPrototypeOf(e);if(!Array.isArray(e)&&r!==Object.prototype&&r!==null)throw new WardConfigError(`${t} must contain only arrays and plain objects`);n.add(e);try{return Object.freeze(Array.isArray(e)?Array.from(e,(e,r)=>snapshotValue(e,`${t}[${r}]`,n)):Object.fromEntries(Object.keys(e).map(r=>[r,snapshotValue(e[r],`${t}.${r}`,n)])))}finally{n.delete(e)}}function snapshotAttributes(e,t){if(e!==void 0){if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${t} must be a plain object`);return snapshotValue(e,t,new Set)}}function compileRule(e,t){let n=`Rule[${t}]`;if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${n} must be a plain object`);let r=Object.getPrototypeOf(e);if(r!==Object.prototype&&r!==null)throw new WardConfigError(`${n} must be a plain object`);if(typeof e.action!=`string`||!e.action.trim())throw new WardConfigError(`${n}.action must be a non-empty string`);if(e.action.endsWith(`:`))throw new WardConfigError(`${n}.action '${e.action}' ends with ':'
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@vielzeug/arsenal");var t=`*`,n=`anonymous`,WardError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},WardConfigError=class extends WardError{},WardConditionError=class extends WardError{ruleIndex;constructor(e,t){super(`Rule[${e}].condition failed`,{cause:t}),this.ruleIndex=e}};function snapshotValue(e,t,n){if(e===null||typeof e==`boolean`||typeof e==`string`)return e;if(typeof e==`number`){if(!Number.isFinite(e))throw new WardConfigError(`${t} must contain only finite numbers`);return e}if(typeof e!=`object`)throw new WardConfigError(`${t} must contain only JSON-compatible values`);if(n.has(e))throw new WardConfigError(`${t} must not contain circular references`);let r=Object.getPrototypeOf(e);if(!Array.isArray(e)&&r!==Object.prototype&&r!==null)throw new WardConfigError(`${t} must contain only arrays and plain objects`);n.add(e);try{return Object.freeze(Array.isArray(e)?Array.from(e,(e,r)=>snapshotValue(e,`${t}[${r}]`,n)):Object.fromEntries(Object.keys(e).map(r=>[r,snapshotValue(e[r],`${t}.${r}`,n)])))}finally{n.delete(e)}}function snapshotAttributes(e,t){if(e!==void 0){if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${t} must be a plain object`);return snapshotValue(e,t,new Set)}}function compileRule(e,t){let n=`Rule[${t}]`;if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${n} must be a plain object`);let r=Object.getPrototypeOf(e);if(r!==Object.prototype&&r!==null)throw new WardConfigError(`${n} must be a plain object`);if(typeof e.action!=`string`||!e.action.trim())throw new WardConfigError(`${n}.action must be a non-empty string`);if(e.action.endsWith(`:`))throw new WardConfigError(`${n}.action '${e.action}' ends with ':': did you mean '${e.action}*'?`);if(typeof e.resource!=`string`||!e.resource.trim())throw new WardConfigError(`${n}.resource must be a non-empty string`);if(e.resource.endsWith(`:`))throw new WardConfigError(`${n}.resource '${e.resource}' ends with ':': did you mean '${e.resource}*'?`);if(e.effect!==`allow`&&e.effect!==`deny`)throw new WardConfigError(`${n}.effect must be "allow" or "deny"`);if(e.condition!==void 0&&typeof e.condition!=`function`)throw new WardConfigError(`${n}.condition must be a function`);if(e.roles!==void 0&&(!Array.isArray(e.roles)||e.roles.some(e=>typeof e!=`string`||!e.trim())))throw new WardConfigError(`${n}.roles must be an array of non-empty strings`);return Object.freeze({action:e.action,...e.attributes===void 0?{}:{attributes:snapshotAttributes(e.attributes,`${n}.attributes`)},...e.condition===void 0?{}:{condition:e.condition},effect:e.effect,resource:e.resource,...e.roles===void 0?{}:{roles:Object.freeze([...e.roles])}})}function matchesPattern(e,t){if(e===`*`||e===t)return!0;if(e.endsWith(`:*`)){let n=e.slice(0,-1);return t.startsWith(n)}return!1}function assertUserPrincipal(e){if(typeof e!=`object`||!e)throw new WardConfigError(`Invalid principal: expected { id: string, roles: string[] }`);let t=e;if(typeof t.id!=`string`||!t.id.trim())throw new WardConfigError(`Invalid principal: id must be a non-empty string`);if(!Array.isArray(t.roles)||t.roles.some(e=>typeof e!=`string`||!e.trim()))throw new WardConfigError(`Invalid principal: roles must be an array of non-empty strings`)}function snapshotPrincipal(e){return e==null?null:(assertUserPrincipal(e),Object.freeze({...e.attributes===void 0?{}:{attributes:snapshotAttributes(e.attributes,`Principal.attributes`)},id:e.id,roles:Object.freeze([...e.roles])}))}function snapshotDecisionInput(e){if(typeof e.action!=`string`||!e.action.trim())throw new WardConfigError(`Decision.action must be a non-empty string`);if(typeof e.resource!=`string`||!e.resource.trim())throw new WardConfigError(`Decision.resource must be a non-empty string`);return Object.freeze({action:e.action,...e.attributes===void 0?{}:{attributes:snapshotAttributes(e.attributes,`Decision.attributes`)},principal:snapshotPrincipal(e.principal),resource:e.resource})}function deepEqual(e,t){if(Object.is(e,t))return!0;if(typeof e!=`object`||typeof t!=`object`||e===null||t===null)return!1;if(Array.isArray(e)||Array.isArray(t))return Array.isArray(e)&&Array.isArray(t)&&e.length===t.length&&e.every((e,n)=>deepEqual(e,t[n]));let n=e,r=t,i=Object.keys(n),a=Object.keys(r);return i.length===a.length&&i.every(e=>Object.hasOwn(r,e)&&deepEqual(n[e],r[e]))}function attributesMatch(e,t){return e?t?Object.keys(e).every(n=>Object.hasOwn(t,n)&&deepEqual(e[n],t[n])):!1:!0}function rolesMatch(e,t){return!e||e.some(e=>e===`anonymous`?t===null:e===`*`?t!==null:t?.roles.includes(e)??!1)}function isThenable(e){return typeof e==`object`&&!!e&&`then`in e&&typeof e.then==`function`||typeof e==`function`&&`then`in e&&typeof e.then==`function`}function ruleMatches(e,t,n){if(!matchesPattern(e.action,n.action)||!matchesPattern(e.resource,n.resource)||!rolesMatch(e.roles,n.principal)||!attributesMatch(e.attributes,n.attributes))return!1;if(!e.condition)return!0;try{let t=e.condition({attributes:n.attributes,principal:n.principal});if(isThenable(t))throw TypeError(`Async conditions are not supported; return a boolean`);if(typeof t!=`boolean`)throw TypeError(`Conditions must return a boolean`);return t}catch(e){throw new WardConditionError(t,e)}}function createWard(t=[]){let n=Object.freeze(t.flat().map((e,t)=>compileRule(e,t))),r=Object.freeze([...new Set(n.flatMap(e=>e.effect===`allow`&&!e.action.includes(`*`)?[e.action]:[]))]),i=(0,e.tapper)();function evaluate(e,t){let r=snapshotDecisionInput(e);for(let e=0;e<n.length;e++){let i=n[e];if(ruleMatches(i,e,r)){let n=Object.freeze({effect:i.effect,matched:!0,reason:`rule[${e}] ${i.effect===`allow`?`allows`:`denies`} '${r.action}' on '${r.resource}'`,rule:i});return t&&emitDecision(r,n),n}}let i=Object.freeze({effect:`deny`,matched:!1,reason:`no matching rule for '${r.action}' on '${r.resource}' (default deny)`});return t&&emitDecision(r,i),i}function emitDecision(e,t){i.emit(Object.freeze({decision:t,input:e,type:`decision`}))}function decide(e){return evaluate(e,!0)}function checkAll(e){return e.map(decide)}function allowedActions(e){let t=e.filter??r,n=new Set;return t.filter(t=>!n.has(t)&&(n.add(t),evaluate({...e,action:t},!1).effect===`allow`))}function forPrincipal(e){let t=snapshotPrincipal(e);return{allowedActions:e=>allowedActions({...e,principal:t}),checkAll:e=>checkAll(e.map(e=>({...e,principal:t}))),decide:e=>decide({...e,principal:t})}}return{allowedActions,checkAll,decide,forPrincipal,knownActions:r,rules:n,tap:(e,t)=>i.tap(e,t)}}function buildRules(e,t,n,r,i){let a=Object.freeze(Array.isArray(t)?[...t]:[t]);return r.map(t=>({action:t,...i?.when===void 0?{}:{condition:i.when},effect:e,resource:n,roles:a}))}var allow=(e,t,n,r)=>buildRules(`allow`,e,t,n,r),deny=(e,t,n,r)=>buildRules(`deny`,e,t,n,r),owns=e=>({attributes:t,principal:n})=>n!==null&&t?.[e]===n.id,and=(...e)=>t=>e.every(e=>e(t)),or=(...e)=>t=>e.some(e=>e(t)),not=e=>t=>!e(t),r={and,not,or,owns};exports.ANONYMOUS=n,exports.WILDCARD=t,exports.WardConditionError=WardConditionError,exports.WardConfigError=WardConfigError,exports.WardError=WardError,exports.allow=allow,exports.createWard=createWard,exports.deny=deny,exports.matchesPattern=matchesPattern,exports.predicate=r;
|
|
2
2
|
//# sourceMappingURL=ward.cjs.map
|
package/dist/ward.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ward.cjs","names":[],"sources":["../src/constants.ts","../src/errors.ts","../src/_compile.ts","../src/resource.ts","../src/_match.ts","../src/factory.ts","../src/rules.ts"],"sourcesContent":["export const WILDCARD = '*' as const;\nexport const ANONYMOUS = 'anonymous' as const;\n","/** 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\n/** Thrown when a rule definition, decision attributes, or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/** Thrown when an application-provided rule condition fails. */\nexport class WardConditionError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n super(`Rule[${ruleIndex}].condition failed`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n","import { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':' — did you mean '${rule.action}*'?`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if (rule.resource.endsWith(':')) {\n throw new WardConfigError(`${at}.resource '${rule.resource}' ends with ':' — did you mean '${rule.resource}*'?`);\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.condition !== undefined && typeof rule.condition !== 'function') {\n throw new WardConfigError(`${at}.condition must be a function`);\n }\n\n if (rule.roles !== undefined) {\n if (!Array.isArray(rule.roles) || rule.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError(`${at}.roles must be an array of non-empty strings`);\n }\n }\n\n return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n ...(rule.roles === undefined ? {} : { roles: Object.freeze([...rule.roles]) }),\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n","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","import { snapshotAttributes } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConditionError, WardConfigError } from './errors';\nimport { matchesPattern } from './resource';\nimport type {\n Principal,\n UserPrincipal,\n WardAttributes,\n WardAttributeValue,\n WardDecisionInput,\n WardRule,\n} from './types';\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const principal = input as Record<string, unknown>;\n\n if (typeof principal.id !== 'string' || !principal.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(principal.roles) || principal.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function snapshotPrincipal(principal: Principal | undefined): Principal {\n if (principal === undefined || principal === null) return null;\n\n assertUserPrincipal(principal);\n\n return Object.freeze({\n ...(principal.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(principal.attributes, 'Principal.attributes') }),\n id: principal.id,\n roles: Object.freeze([...principal.roles]),\n });\n}\n\nexport function snapshotDecisionInput<\n TAction extends string,\n TResource extends string,\n TAttributes extends WardAttributes,\n>(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n): WardDecisionInput<TAction, TResource, TAttributes> & {\n principal: Principal;\n} {\n if (typeof input.action !== 'string' || !input.action.trim()) {\n throw new WardConfigError('Decision.action must be a non-empty string');\n }\n if (typeof input.resource !== 'string' || !input.resource.trim()) {\n throw new WardConfigError('Decision.resource must be a non-empty string');\n }\n\n return Object.freeze({\n action: input.action,\n ...(input.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(input.attributes, 'Decision.attributes') as TAttributes }),\n principal: snapshotPrincipal(input.principal),\n resource: input.resource,\n });\n}\n\nfunction deepEqual(left: WardAttributeValue, right: WardAttributeValue): boolean {\n if (Object.is(left, right)) return true;\n if (typeof left !== 'object' || typeof right !== 'object' || left === null || right === null) return false;\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((value, index) => deepEqual(value, right[index]))\n );\n }\n\n const leftRecord = left as Record<string, WardAttributeValue>;\n const rightRecord = right as Record<string, WardAttributeValue>;\n const leftKeys = Object.keys(leftRecord);\n const rightKeys = Object.keys(rightRecord);\n\n return (\n leftKeys.length === rightKeys.length &&\n leftKeys.every((key) => Object.hasOwn(rightRecord, key) && deepEqual(leftRecord[key], rightRecord[key]))\n );\n}\n\nexport function attributesMatch(expected: WardAttributes | undefined, actual: WardAttributes | undefined): boolean {\n if (!expected) return true;\n if (!actual) return false;\n\n return Object.keys(expected).every((key) => Object.hasOwn(actual, key) && deepEqual(expected[key], actual[key]));\n}\n\n/**\n * Declarative role check: `ANONYMOUS` matches a null principal, `WILDCARD`\n * matches any authenticated principal, an exact role must be in the principal's\n * roles. A rule without `roles` matches every principal.\n */\nfunction rolesMatch(expected: readonly string[] | undefined, principal: Principal): boolean {\n if (!expected) return true;\n\n return expected.some((candidate) =>\n candidate === ANONYMOUS\n ? principal === null\n : candidate === WILDCARD\n ? principal !== null\n : (principal?.roles.includes(candidate) ?? false),\n );\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n (typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function') ||\n (typeof value === 'function' && 'then' in value && typeof value.then === 'function')\n );\n}\n\nexport function ruleMatches<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n): boolean {\n if (!matchesPattern(rule.action, input.action)) return false;\n if (!matchesPattern(rule.resource, input.resource)) return false;\n if (!rolesMatch(rule.roles, input.principal)) return false;\n if (!attributesMatch(rule.attributes, input.attributes)) return false;\n if (!rule.condition) return true;\n\n try {\n const result: unknown = rule.condition({ attributes: input.attributes, principal: input.principal });\n\n if (isThenable(result)) {\n throw new TypeError('Async conditions are not supported; return a boolean');\n }\n if (typeof result !== 'boolean') {\n throw new TypeError('Conditions must return a boolean');\n }\n\n return result;\n } catch (error) {\n throw new WardConditionError(index, error);\n }\n}\n","import { tapper } from '@vielzeug/arsenal';\n\nimport { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport type {\n BoundWard,\n Principal,\n Ward,\n WardAttributes,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardRule,\n} from './types';\n\n/**\n * Creates an authorization ward from an ordered list of rules.\n *\n * **Decision model**: rules are evaluated in declaration order; the first rule\n * that matches the request wins. If no rule matches, the decision is **deny**\n * (default deny). There is no priority, specificity scoring, or conflict\n * detection — order is the only tiebreaker.\n *\n * A rule matches when:\n * 1. Its `action` pattern matches the requested action (`*` or `ns:*` or exact).\n * 2. Its `resource` pattern matches the requested resource.\n * 3. Its declarative `roles` (if any) include the principal's role, `ANONYMOUS`\n * matches a null principal, and `WILDCARD` matches any authenticated principal.\n * 4. Every key in its declarative `attributes` is present and equal in the\n * request's `attributes` (deep equality).\n * 5. Its `condition` escape-hatch callback (if any) returns `true`.\n */\nexport function createWard<\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n rules: readonly (\n | WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>\n | readonly WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>[]\n )[] = [],\n): Ward<TAction, TResource, TAttributes> {\n const compiled = Object.freeze(rules.flat().map((rule, index) => compileRule(rule, index)));\n const knownActions = Object.freeze([\n ...new Set(\n compiled.flatMap((rule) => (rule.effect === 'allow' && !rule.action.includes('*') ? [rule.action] : [])),\n ),\n ]) as readonly TAction[];\n const tappers = tapper<WardEvent<TAction, TResource, TAttributes>>();\n\n function evaluate(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n emit: boolean,\n ): WardDecision<TAction, TResource, TAttributes> {\n const normalized = snapshotDecisionInput(input);\n\n for (let index = 0; index < compiled.length; index++) {\n const rule = compiled[index];\n\n if (ruleMatches(rule, index, normalized)) {\n const decision = Object.freeze({\n effect: rule.effect,\n matched: true,\n reason: `rule[${index}] ${rule.effect === 'allow' ? 'allows' : 'denies'} '${normalized.action}' on '${normalized.resource}'`,\n rule,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n }\n\n const decision = Object.freeze({\n effect: 'deny',\n matched: false,\n reason: `no matching rule for '${normalized.action}' on '${normalized.resource}' (default deny)`,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n\n function emitDecision(\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n decision: WardDecision<TAction, TResource, TAttributes>,\n ): void {\n tappers.emit(Object.freeze({ decision, input, type: 'decision' as const }));\n }\n\n function decide(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n ): WardDecision<TAction, TResource, TAttributes> {\n return evaluate(input, true);\n }\n\n function checkAll(\n inputs: readonly WardDecisionInput<TAction, TResource, TAttributes>[],\n ): WardDecision<TAction, TResource, TAttributes>[] {\n return inputs.map(decide);\n }\n\n function allowedActions(input: {\n attributes?: TAttributes;\n filter?: readonly TAction[];\n principal?: Principal;\n resource: TResource;\n }): TAction[] {\n const candidates = input.filter ?? knownActions;\n const seen = new Set<TAction>();\n\n return candidates.filter((action) => {\n if (seen.has(action)) return false;\n seen.add(action);\n return evaluate({ ...input, action }, false).effect === 'allow';\n });\n }\n\n function forPrincipal(principal?: Principal): BoundWard<TAction, TResource, TAttributes> {\n const snapshot = snapshotPrincipal(principal);\n\n return {\n allowedActions: (input) => allowedActions({ ...input, principal: snapshot }),\n checkAll: (inputs) => checkAll(inputs.map((input) => ({ ...input, principal: snapshot }))),\n decide: (input) => decide({ ...input, principal: snapshot }),\n };\n }\n\n return {\n allowedActions,\n checkAll,\n decide,\n forPrincipal,\n knownActions,\n rules: compiled,\n tap: (handler, options) => tappers.tap(handler, options),\n };\n}\n","import type { WardAttributes, WardCondition, WardPattern, WardRule } from './types';\n\ntype RuleOptions<TAttributes extends WardAttributes> = Readonly<{\n when?: WardCondition<TAttributes>;\n}>;\n\nfunction buildRules<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] {\n const roles = Object.freeze(Array.isArray(role) ? [...role] : [role]);\n\n return actions.map((action) => ({\n action,\n ...(options?.when === undefined ? {} : { condition: options.when }),\n effect,\n resource,\n roles,\n }));\n}\n\nexport const allow = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('allow', role, resource, actions, options);\n\nexport const deny = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('deny', role, resource, actions, options);\n\nconst owns =\n <TAttributes extends WardAttributes = WardAttributes>(\n attribute: keyof TAttributes & string,\n ): WardCondition<TAttributes> =>\n ({ attributes, principal }) =>\n principal !== null && attributes?.[attribute] === principal.id;\nconst and =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.every((condition) => condition(input));\nconst or =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.some((condition) => condition(input));\nconst not =\n <TAttributes extends WardAttributes = WardAttributes>(\n condition: WardCondition<TAttributes>,\n ): WardCondition<TAttributes> =>\n (input) =>\n !condition(input);\n\nexport const predicate = { and, not, or, owns } as const;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ward.cjs","names":[],"sources":["../src/constants.ts","../src/errors.ts","../src/_compile.ts","../src/resource.ts","../src/_match.ts","../src/factory.ts","../src/rules.ts"],"sourcesContent":["export const WILDCARD = '*' as const;\nexport const ANONYMOUS = 'anonymous' as const;\n","/** 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\n/** Thrown when a rule definition, decision attributes, or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/** Thrown when an application-provided rule condition fails. */\nexport class WardConditionError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n super(`Rule[${ruleIndex}].condition failed`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n","import { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':': did you mean '${rule.action}*'?`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if (rule.resource.endsWith(':')) {\n throw new WardConfigError(`${at}.resource '${rule.resource}' ends with ':': did you mean '${rule.resource}*'?`);\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.condition !== undefined && typeof rule.condition !== 'function') {\n throw new WardConfigError(`${at}.condition must be a function`);\n }\n\n if (rule.roles !== undefined) {\n if (!Array.isArray(rule.roles) || rule.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError(`${at}.roles must be an array of non-empty strings`);\n }\n }\n\n return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n ...(rule.roles === undefined ? {} : { roles: Object.freeze([...rule.roles]) }),\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n","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","import { snapshotAttributes } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConditionError, WardConfigError } from './errors';\nimport { matchesPattern } from './resource';\nimport type {\n Principal,\n UserPrincipal,\n WardAttributes,\n WardAttributeValue,\n WardDecisionInput,\n WardRule,\n} from './types';\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const principal = input as Record<string, unknown>;\n\n if (typeof principal.id !== 'string' || !principal.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(principal.roles) || principal.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function snapshotPrincipal(principal: Principal | undefined): Principal {\n if (principal === undefined || principal === null) return null;\n\n assertUserPrincipal(principal);\n\n return Object.freeze({\n ...(principal.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(principal.attributes, 'Principal.attributes') }),\n id: principal.id,\n roles: Object.freeze([...principal.roles]),\n });\n}\n\nexport function snapshotDecisionInput<\n TAction extends string,\n TResource extends string,\n TAttributes extends WardAttributes,\n>(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n): WardDecisionInput<TAction, TResource, TAttributes> & {\n principal: Principal;\n} {\n if (typeof input.action !== 'string' || !input.action.trim()) {\n throw new WardConfigError('Decision.action must be a non-empty string');\n }\n if (typeof input.resource !== 'string' || !input.resource.trim()) {\n throw new WardConfigError('Decision.resource must be a non-empty string');\n }\n\n return Object.freeze({\n action: input.action,\n ...(input.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(input.attributes, 'Decision.attributes') as TAttributes }),\n principal: snapshotPrincipal(input.principal),\n resource: input.resource,\n });\n}\n\nfunction deepEqual(left: WardAttributeValue, right: WardAttributeValue): boolean {\n if (Object.is(left, right)) return true;\n if (typeof left !== 'object' || typeof right !== 'object' || left === null || right === null) return false;\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((value, index) => deepEqual(value, right[index]))\n );\n }\n\n const leftRecord = left as Record<string, WardAttributeValue>;\n const rightRecord = right as Record<string, WardAttributeValue>;\n const leftKeys = Object.keys(leftRecord);\n const rightKeys = Object.keys(rightRecord);\n\n return (\n leftKeys.length === rightKeys.length &&\n leftKeys.every((key) => Object.hasOwn(rightRecord, key) && deepEqual(leftRecord[key], rightRecord[key]))\n );\n}\n\nexport function attributesMatch(expected: WardAttributes | undefined, actual: WardAttributes | undefined): boolean {\n if (!expected) return true;\n if (!actual) return false;\n\n return Object.keys(expected).every((key) => Object.hasOwn(actual, key) && deepEqual(expected[key], actual[key]));\n}\n\n/**\n * Declarative role check: `ANONYMOUS` matches a null principal, `WILDCARD`\n * matches any authenticated principal, an exact role must be in the principal's\n * roles. A rule without `roles` matches every principal.\n */\nfunction rolesMatch(expected: readonly string[] | undefined, principal: Principal): boolean {\n if (!expected) return true;\n\n return expected.some((candidate) =>\n candidate === ANONYMOUS\n ? principal === null\n : candidate === WILDCARD\n ? principal !== null\n : (principal?.roles.includes(candidate) ?? false),\n );\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n (typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function') ||\n (typeof value === 'function' && 'then' in value && typeof value.then === 'function')\n );\n}\n\nexport function ruleMatches<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n): boolean {\n if (!matchesPattern(rule.action, input.action)) return false;\n if (!matchesPattern(rule.resource, input.resource)) return false;\n if (!rolesMatch(rule.roles, input.principal)) return false;\n if (!attributesMatch(rule.attributes, input.attributes)) return false;\n if (!rule.condition) return true;\n\n try {\n const result: unknown = rule.condition({ attributes: input.attributes, principal: input.principal });\n\n if (isThenable(result)) {\n throw new TypeError('Async conditions are not supported; return a boolean');\n }\n if (typeof result !== 'boolean') {\n throw new TypeError('Conditions must return a boolean');\n }\n\n return result;\n } catch (error) {\n throw new WardConditionError(index, error);\n }\n}\n","import { tapper } from '@vielzeug/arsenal';\n\nimport { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport type {\n BoundWard,\n Principal,\n Ward,\n WardAttributes,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardRule,\n} from './types';\n\n/**\n * Creates an authorization ward from an ordered list of rules.\n *\n * **Decision model**: rules are evaluated in declaration order; the first rule\n * that matches the request wins. If no rule matches, the decision is **deny**\n * (default deny). There is no priority, specificity scoring, or conflict\n * detection: order is the only tiebreaker.\n *\n * A rule matches when:\n * 1. Its `action` pattern matches the requested action (`*` or `ns:*` or exact).\n * 2. Its `resource` pattern matches the requested resource.\n * 3. Its declarative `roles` (if any) include the principal's role, `ANONYMOUS`\n * matches a null principal, and `WILDCARD` matches any authenticated principal.\n * 4. Every key in its declarative `attributes` is present and equal in the\n * request's `attributes` (deep equality).\n * 5. Its `condition` escape-hatch callback (if any) returns `true`.\n */\nexport function createWard<\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n rules: readonly (\n | WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>\n | readonly WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>[]\n )[] = [],\n): Ward<TAction, TResource, TAttributes> {\n const compiled = Object.freeze(rules.flat().map((rule, index) => compileRule(rule, index)));\n const knownActions = Object.freeze([\n ...new Set(\n compiled.flatMap((rule) => (rule.effect === 'allow' && !rule.action.includes('*') ? [rule.action] : [])),\n ),\n ]) as readonly TAction[];\n const tappers = tapper<WardEvent<TAction, TResource, TAttributes>>();\n\n function evaluate(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n emit: boolean,\n ): WardDecision<TAction, TResource, TAttributes> {\n const normalized = snapshotDecisionInput(input);\n\n for (let index = 0; index < compiled.length; index++) {\n const rule = compiled[index];\n\n if (ruleMatches(rule, index, normalized)) {\n const decision = Object.freeze({\n effect: rule.effect,\n matched: true,\n reason: `rule[${index}] ${rule.effect === 'allow' ? 'allows' : 'denies'} '${normalized.action}' on '${normalized.resource}'`,\n rule,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n }\n\n const decision = Object.freeze({\n effect: 'deny',\n matched: false,\n reason: `no matching rule for '${normalized.action}' on '${normalized.resource}' (default deny)`,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n\n function emitDecision(\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n decision: WardDecision<TAction, TResource, TAttributes>,\n ): void {\n tappers.emit(Object.freeze({ decision, input, type: 'decision' as const }));\n }\n\n function decide(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n ): WardDecision<TAction, TResource, TAttributes> {\n return evaluate(input, true);\n }\n\n function checkAll(\n inputs: readonly WardDecisionInput<TAction, TResource, TAttributes>[],\n ): WardDecision<TAction, TResource, TAttributes>[] {\n return inputs.map(decide);\n }\n\n function allowedActions(input: {\n attributes?: TAttributes;\n filter?: readonly TAction[];\n principal?: Principal;\n resource: TResource;\n }): TAction[] {\n const candidates = input.filter ?? knownActions;\n const seen = new Set<TAction>();\n\n return candidates.filter((action) => {\n if (seen.has(action)) return false;\n seen.add(action);\n return evaluate({ ...input, action }, false).effect === 'allow';\n });\n }\n\n function forPrincipal(principal?: Principal): BoundWard<TAction, TResource, TAttributes> {\n const snapshot = snapshotPrincipal(principal);\n\n return {\n allowedActions: (input) => allowedActions({ ...input, principal: snapshot }),\n checkAll: (inputs) => checkAll(inputs.map((input) => ({ ...input, principal: snapshot }))),\n decide: (input) => decide({ ...input, principal: snapshot }),\n };\n }\n\n return {\n allowedActions,\n checkAll,\n decide,\n forPrincipal,\n knownActions,\n rules: compiled,\n tap: (handler, options) => tappers.tap(handler, options),\n };\n}\n","import type { WardAttributes, WardCondition, WardPattern, WardRule } from './types';\n\ntype RuleOptions<TAttributes extends WardAttributes> = Readonly<{\n when?: WardCondition<TAttributes>;\n}>;\n\nfunction buildRules<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] {\n const roles = Object.freeze(Array.isArray(role) ? [...role] : [role]);\n\n return actions.map((action) => ({\n action,\n ...(options?.when === undefined ? {} : { condition: options.when }),\n effect,\n resource,\n roles,\n }));\n}\n\nexport const allow = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('allow', role, resource, actions, options);\n\nexport const deny = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('deny', role, resource, actions, options);\n\nconst owns =\n <TAttributes extends WardAttributes = WardAttributes>(\n attribute: keyof TAttributes & string,\n ): WardCondition<TAttributes> =>\n ({ attributes, principal }) =>\n principal !== null && attributes?.[attribute] === principal.id;\nconst and =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.every((condition) => condition(input));\nconst or =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.some((condition) => condition(input));\nconst not =\n <TAttributes extends WardAttributes = WardAttributes>(\n condition: WardCondition<TAttributes>,\n ): WardCondition<TAttributes> =>\n (input) =>\n !condition(input);\n\nexport const predicate = { and, not, or, owns } as const;\n"],"mappings":"sGAAA,IAAa,EAAW,IACX,EAAY,YCAZ,UAAb,cAA+B,KAAM,CACnC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CACF,EAGa,gBAAb,cAAqC,SAAU,CAAC,EAGnC,mBAAb,cAAwC,SAAU,CAChD,UAEA,YAAY,EAAmB,EAAgB,CAC7C,MAAM,QAAQ,EAAU,oBAAqB,CAAE,OAAM,CAAC,EACtD,KAAK,UAAY,CACnB,CACF,ECjBA,SAAS,cAAc,EAAgB,EAAc,EAA4C,CAC/F,GAAI,IAAU,MAAQ,OAAO,GAAU,WAAa,OAAO,GAAU,SAAU,OAAO,EAEtF,GAAI,OAAO,GAAU,SAAU,CAC7B,GAAI,CAAC,OAAO,SAAS,CAAK,EAAG,MAAM,IAAI,gBAAgB,GAAG,EAAK,kCAAkC,EACjG,OAAO,CACT,CAEA,GAAI,OAAO,GAAU,SACnB,MAAM,IAAI,gBAAgB,GAAG,EAAK,0CAA0C,EAG9E,GAAI,EAAU,IAAI,CAAK,EAAG,MAAM,IAAI,gBAAgB,GAAG,EAAK,sCAAsC,EAElG,IAAM,EAAY,OAAO,eAAe,CAAK,EAC7C,GAAI,CAAC,MAAM,QAAQ,CAAK,GAAK,IAAc,OAAO,WAAa,IAAc,KAC3E,MAAM,IAAI,gBAAgB,GAAG,EAAK,4CAA4C,EAGhF,EAAU,IAAI,CAAK,EAEnB,GAAI,CAKF,OAHS,OAAO,OADZ,MAAM,QAAQ,CAAK,EACA,MAAM,KAAK,GAAQ,EAAM,IAAU,cAAc,EAAM,GAAG,EAAK,GAAG,EAAM,GAAI,CAAS,CAAC,EAI3G,OAAO,YACL,OAAO,KAAK,CAAK,CAAC,CAAC,IAAK,GAAQ,CAC9B,EACA,cAAe,EAAkC,GAAM,GAAG,EAAK,GAAG,IAAO,CAAS,CACpF,CAAC,CACH,CAT4G,CAWhH,QAAU,CACR,EAAU,OAAO,CAAK,CACxB,CACF,CAEA,SAAgB,mBAAmB,EAAgB,EAA0C,CACvF,OAAU,IAAA,GACd,IAAI,OAAO,GAAU,WAAY,GAAkB,MAAM,QAAQ,CAAK,EACpE,MAAM,IAAI,gBAAgB,GAAG,EAAK,wBAAwB,EAG5D,OAAO,cAAc,EAAO,EAAM,IAAI,GAAK,CAHiB,CAI9D,CAMA,SAAgB,YACd,EACA,EAC2C,CAC3C,IAAM,EAAK,QAAQ,EAAM,GAEzB,GAAI,OAAO,GAAS,WAAY,GAAiB,MAAM,QAAQ,CAAI,EACjE,MAAM,IAAI,gBAAgB,GAAG,EAAG,wBAAwB,EAE1D,IAAM,EAAY,OAAO,eAAe,CAAI,EAC5C,GAAI,IAAc,OAAO,WAAa,IAAc,KAClD,MAAM,IAAI,gBAAgB,GAAG,EAAG,wBAAwB,EAG1D,GAAI,OAAO,EAAK,QAAW,UAAY,CAAC,EAAK,OAAO,KAAK,EACvD,MAAM,IAAI,gBAAgB,GAAG,EAAG,mCAAmC,EAGrE,GAAI,EAAK,OAAO,SAAS,GAAG,EAC1B,MAAM,IAAI,gBAAgB,GAAG,EAAG,WAAW,EAAK,OAAO,iCAAiC,EAAK,OAAO,IAAI,EAG1G,GAAI,OAAO,EAAK,UAAa,UAAY,CAAC,EAAK,SAAS,KAAK,EAC3D,MAAM,IAAI,gBAAgB,GAAG,EAAG,qCAAqC,EAGvE,GAAI,EAAK,SAAS,SAAS,GAAG,EAC5B,MAAM,IAAI,gBAAgB,GAAG,EAAG,aAAa,EAAK,SAAS,iCAAiC,EAAK,SAAS,IAAI,EAGhH,GAAI,EAAK,SAAW,SAAW,EAAK,SAAW,OAC7C,MAAM,IAAI,gBAAgB,GAAG,EAAG,kCAAkC,EAGpE,GAAI,EAAK,YAAc,IAAA,IAAa,OAAO,EAAK,WAAc,WAC5D,MAAM,IAAI,gBAAgB,GAAG,EAAG,8BAA8B,EAGhE,GAAI,EAAK,QAAU,IAAA,KACb,CAAC,MAAM,QAAQ,EAAK,KAAK,GAAK,EAAK,MAAM,KAAM,GAAS,OAAO,GAAS,UAAY,CAAC,EAAK,KAAK,CAAC,GAClG,MAAM,IAAI,gBAAgB,GAAG,EAAG,6CAA6C,EAIjF,OAAO,OAAO,OAAO,CACnB,OAAQ,EAAK,OACb,GAAI,EAAK,aAAe,IAAA,GAAY,CAAC,EAAI,CAAE,WAAY,mBAAmB,EAAK,WAAY,GAAG,EAAG,YAAY,CAAE,EAC/G,GAAI,EAAK,YAAc,IAAA,GAAY,CAAC,EAAI,CAAE,UAAW,EAAK,SAAU,EACpE,OAAQ,EAAK,OACb,SAAU,EAAK,SACf,GAAI,EAAK,QAAU,IAAA,GAAY,CAAC,EAAI,CAAE,MAAO,OAAO,OAAO,CAAC,GAAG,EAAK,KAAK,CAAC,CAAE,CAC9E,CAAC,CACH,CCpFA,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,CCtBA,SAAgB,oBAAoB,EAAgD,CAClF,GAAI,OAAO,GAAU,UAAY,CAAC,EAChC,MAAM,IAAI,gBAAgB,6DAA6D,EAGzF,IAAM,EAAY,EAElB,GAAI,OAAO,EAAU,IAAO,UAAY,CAAC,EAAU,GAAG,KAAK,EACzD,MAAM,IAAI,gBAAgB,kDAAkD,EAG9E,GAAI,CAAC,MAAM,QAAQ,EAAU,KAAK,GAAK,EAAU,MAAM,KAAM,GAAS,OAAO,GAAS,UAAY,CAAC,EAAK,KAAK,CAAC,EAC5G,MAAM,IAAI,gBAAgB,gEAAgE,CAE9F,CAEA,SAAgB,kBAAkB,EAA6C,CAK7E,OAJI,GAAyC,KAAa,MAE1D,oBAAoB,CAAS,EAEtB,OAAO,OAAO,CACnB,GAAI,EAAU,aAAe,IAAA,GACzB,CAAC,EACD,CAAE,WAAY,mBAAmB,EAAU,WAAY,sBAAsB,CAAE,EACnF,GAAI,EAAU,GACd,MAAO,OAAO,OAAO,CAAC,GAAG,EAAU,KAAK,CAAC,CAC3C,CAAC,EACH,CAEA,SAAgB,sBAKd,EAGA,CACA,GAAI,OAAO,EAAM,QAAW,UAAY,CAAC,EAAM,OAAO,KAAK,EACzD,MAAM,IAAI,gBAAgB,4CAA4C,EAExE,GAAI,OAAO,EAAM,UAAa,UAAY,CAAC,EAAM,SAAS,KAAK,EAC7D,MAAM,IAAI,gBAAgB,8CAA8C,EAG1E,OAAO,OAAO,OAAO,CACnB,OAAQ,EAAM,OACd,GAAI,EAAM,aAAe,IAAA,GACrB,CAAC,EACD,CAAE,WAAY,mBAAmB,EAAM,WAAY,qBAAqB,CAAiB,EAC7F,UAAW,kBAAkB,EAAM,SAAS,EAC5C,SAAU,EAAM,QAClB,CAAC,CACH,CAEA,SAAS,UAAU,EAA0B,EAAoC,CAC/E,GAAI,OAAO,GAAG,EAAM,CAAK,EAAG,MAAO,GACnC,GAAI,OAAO,GAAS,UAAY,OAAO,GAAU,UAAY,IAAS,MAAQ,IAAU,KAAM,MAAO,GACrG,GAAI,MAAM,QAAQ,CAAI,GAAK,MAAM,QAAQ,CAAK,EAC5C,OACE,MAAM,QAAQ,CAAI,GAClB,MAAM,QAAQ,CAAK,GACnB,EAAK,SAAW,EAAM,QACtB,EAAK,OAAO,EAAO,IAAU,UAAU,EAAO,EAAM,EAAM,CAAC,EAI/D,IAAM,EAAa,EACb,EAAc,EACd,EAAW,OAAO,KAAK,CAAU,EACjC,EAAY,OAAO,KAAK,CAAW,EAEzC,OACE,EAAS,SAAW,EAAU,QAC9B,EAAS,MAAO,GAAQ,OAAO,OAAO,EAAa,CAAG,GAAK,UAAU,EAAW,GAAM,EAAY,EAAI,CAAC,CAE3G,CAEA,SAAgB,gBAAgB,EAAsC,EAA6C,CAIjH,OAHK,EACA,EAEE,OAAO,KAAK,CAAQ,CAAC,CAAC,MAAO,GAAQ,OAAO,OAAO,EAAQ,CAAG,GAAK,UAAU,EAAS,GAAM,EAAO,EAAI,CAAC,EAF3F,GADE,EAIxB,CAOA,SAAS,WAAW,EAAyC,EAA+B,CAG1F,MAFA,CAAK,GAEE,EAAS,KAAM,GACpB,IAAA,YACI,IAAc,KACd,IAAA,IACE,IAAc,KACb,GAAW,MAAM,SAAS,CAAS,GAAK,EACjD,CACF,CAEA,SAAS,WAAW,EAA+C,CACjE,OACG,OAAO,GAAU,YAAY,GAAkB,SAAU,GAAS,OAAO,EAAM,MAAS,YACxF,OAAO,GAAU,YAAc,SAAU,GAAS,OAAO,EAAM,MAAS,UAE7E,CAEA,SAAgB,YACd,EACA,EACA,EACS,CAIT,GAHI,CAAC,eAAe,EAAK,OAAQ,EAAM,MAAM,GACzC,CAAC,eAAe,EAAK,SAAU,EAAM,QAAQ,GAC7C,CAAC,WAAW,EAAK,MAAO,EAAM,SAAS,GACvC,CAAC,gBAAgB,EAAK,WAAY,EAAM,UAAU,EAAG,MAAO,GAChE,GAAI,CAAC,EAAK,UAAW,MAAO,GAE5B,GAAI,CACF,IAAM,EAAkB,EAAK,UAAU,CAAE,WAAY,EAAM,WAAY,UAAW,EAAM,SAAU,CAAC,EAEnG,GAAI,WAAW,CAAM,EACnB,MAAU,UAAU,sDAAsD,EAE5E,GAAI,OAAO,GAAW,UACpB,MAAU,UAAU,kCAAkC,EAGxD,OAAO,CACT,OAAS,EAAO,CACd,MAAM,IAAI,mBAAmB,EAAO,CAAK,CAC3C,CACF,CCrHA,SAAgB,WAKd,EAGM,CAAC,EACgC,CACvC,IAAM,EAAW,OAAO,OAAO,EAAM,KAAK,CAAC,CAAC,KAAK,EAAM,IAAU,YAAY,EAAM,CAAK,CAAC,CAAC,EACpF,EAAe,OAAO,OAAO,CACjC,GAAG,IAAI,IACL,EAAS,QAAS,GAAU,EAAK,SAAW,SAAW,CAAC,EAAK,OAAO,SAAS,GAAG,EAAI,CAAC,EAAK,MAAM,EAAI,CAAC,CAAE,CACzG,CACF,CAAC,EACK,GAAA,EAAU,EAAA,OAAA,CAAmD,EAEnE,SAAS,SACP,EACA,EAC+C,CAC/C,IAAM,EAAa,sBAAsB,CAAK,EAE9C,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAS,OAAQ,IAAS,CACpD,IAAM,EAAO,EAAS,GAEtB,GAAI,YAAY,EAAM,EAAO,CAAU,EAAG,CACxC,IAAM,EAAW,OAAO,OAAO,CAC7B,OAAQ,EAAK,OACb,QAAS,GACT,OAAQ,QAAQ,EAAM,IAAI,EAAK,SAAW,QAAU,SAAW,SAAS,IAAI,EAAW,OAAO,QAAQ,EAAW,SAAS,GAC1H,MACF,CAAC,EAGD,OADI,GAAM,aAAa,EAAY,CAAQ,EACpC,CACT,CACF,CAEA,IAAM,EAAW,OAAO,OAAO,CAC7B,OAAQ,OACR,QAAS,GACT,OAAQ,yBAAyB,EAAW,OAAO,QAAQ,EAAW,SAAS,iBACjF,CAAC,EAGD,OADI,GAAM,aAAa,EAAY,CAAQ,EACpC,CACT,CAEA,SAAS,aACP,EACA,EACM,CACN,EAAQ,KAAK,OAAO,OAAO,CAAE,WAAU,QAAO,KAAM,UAAoB,CAAC,CAAC,CAC5E,CAEA,SAAS,OACP,EAC+C,CAC/C,OAAO,SAAS,EAAO,EAAI,CAC7B,CAEA,SAAS,SACP,EACiD,CACjD,OAAO,EAAO,IAAI,MAAM,CAC1B,CAEA,SAAS,eAAe,EAKV,CACZ,IAAM,EAAa,EAAM,QAAU,EAC7B,EAAO,IAAI,IAEjB,OAAO,EAAW,OAAQ,GACxB,CAAI,EAAK,IAAI,CAAM,IACnB,EAAK,IAAI,CAAM,EACR,SAAS,CAAE,GAAG,EAAO,QAAO,EAAG,EAAK,CAAC,CAAC,SAAW,QACzD,CACH,CAEA,SAAS,aAAa,EAAmE,CACvF,IAAM,EAAW,kBAAkB,CAAS,EAE5C,MAAO,CACL,eAAiB,GAAU,eAAe,CAAE,GAAG,EAAO,UAAW,CAAS,CAAC,EAC3E,SAAW,GAAW,SAAS,EAAO,IAAK,IAAW,CAAE,GAAG,EAAO,UAAW,CAAS,EAAE,CAAC,EACzF,OAAS,GAAU,OAAO,CAAE,GAAG,EAAO,UAAW,CAAS,CAAC,CAC7D,CACF,CAEA,MAAO,CACL,eACA,SACA,OACA,aACA,eACA,MAAO,EACP,KAAM,EAAS,IAAY,EAAQ,IAAI,EAAS,CAAO,CACzD,CACF,CClIA,SAAS,WACP,EACA,EACA,EACA,EACA,EAC6C,CAC7C,IAAM,EAAQ,OAAO,OAAO,MAAM,QAAQ,CAAI,EAAI,CAAC,GAAG,CAAI,EAAI,CAAC,CAAI,CAAC,EAEpE,OAAO,EAAQ,IAAK,IAAY,CAC9B,SACA,GAAI,GAAS,OAAS,IAAA,GAAY,CAAC,EAAI,CAAE,UAAW,EAAQ,IAAK,EACjE,SACA,WACA,OACF,EAAE,CACJ,CAEA,IAAa,OAKX,EACA,EACA,EACA,IACgD,WAAW,QAAS,EAAM,EAAU,EAAS,CAAO,EAEzF,MAKX,EACA,EACA,EACA,IACgD,WAAW,OAAQ,EAAM,EAAU,EAAS,CAAO,EAE/F,KAEF,IAED,CAAE,aAAY,eACb,IAAc,MAAQ,IAAa,KAAe,EAAU,GAC1D,KAEF,GAAG,IAEJ,GACC,EAAW,MAAO,GAAc,EAAU,CAAK,CAAC,EAC9C,IAEF,GAAG,IAEJ,GACC,EAAW,KAAM,GAAc,EAAU,CAAK,CAAC,EAC7C,IAEF,GAED,GACC,CAAC,EAAU,CAAK,EAEP,EAAY,CAAE,IAAK,IAAK,GAAI,IAAK"}
|
package/dist/ward.iife.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var Ward=(function(e,t){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var n=`*`,r=`anonymous`,WardError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},WardConfigError=class extends WardError{},WardConditionError=class extends WardError{ruleIndex;constructor(e,t){super(`Rule[${e}].condition failed`,{cause:t}),this.ruleIndex=e}};function snapshotValue(e,t,n){if(e===null||typeof e==`boolean`||typeof e==`string`)return e;if(typeof e==`number`){if(!Number.isFinite(e))throw new WardConfigError(`${t} must contain only finite numbers`);return e}if(typeof e!=`object`)throw new WardConfigError(`${t} must contain only JSON-compatible values`);if(n.has(e))throw new WardConfigError(`${t} must not contain circular references`);let r=Object.getPrototypeOf(e);if(!Array.isArray(e)&&r!==Object.prototype&&r!==null)throw new WardConfigError(`${t} must contain only arrays and plain objects`);n.add(e);try{return Object.freeze(Array.isArray(e)?Array.from(e,(e,r)=>snapshotValue(e,`${t}[${r}]`,n)):Object.fromEntries(Object.keys(e).map(r=>[r,snapshotValue(e[r],`${t}.${r}`,n)])))}finally{n.delete(e)}}function snapshotAttributes(e,t){if(e!==void 0){if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${t} must be a plain object`);return snapshotValue(e,t,new Set)}}function compileRule(e,t){let n=`Rule[${t}]`;if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${n} must be a plain object`);let r=Object.getPrototypeOf(e);if(r!==Object.prototype&&r!==null)throw new WardConfigError(`${n} must be a plain object`);if(typeof e.action!=`string`||!e.action.trim())throw new WardConfigError(`${n}.action must be a non-empty string`);if(e.action.endsWith(`:`))throw new WardConfigError(`${n}.action '${e.action}' ends with ':'
|
|
1
|
+
var Ward=(function(e,t){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var n=`*`,r=`anonymous`,WardError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},WardConfigError=class extends WardError{},WardConditionError=class extends WardError{ruleIndex;constructor(e,t){super(`Rule[${e}].condition failed`,{cause:t}),this.ruleIndex=e}};function snapshotValue(e,t,n){if(e===null||typeof e==`boolean`||typeof e==`string`)return e;if(typeof e==`number`){if(!Number.isFinite(e))throw new WardConfigError(`${t} must contain only finite numbers`);return e}if(typeof e!=`object`)throw new WardConfigError(`${t} must contain only JSON-compatible values`);if(n.has(e))throw new WardConfigError(`${t} must not contain circular references`);let r=Object.getPrototypeOf(e);if(!Array.isArray(e)&&r!==Object.prototype&&r!==null)throw new WardConfigError(`${t} must contain only arrays and plain objects`);n.add(e);try{return Object.freeze(Array.isArray(e)?Array.from(e,(e,r)=>snapshotValue(e,`${t}[${r}]`,n)):Object.fromEntries(Object.keys(e).map(r=>[r,snapshotValue(e[r],`${t}.${r}`,n)])))}finally{n.delete(e)}}function snapshotAttributes(e,t){if(e!==void 0){if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${t} must be a plain object`);return snapshotValue(e,t,new Set)}}function compileRule(e,t){let n=`Rule[${t}]`;if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${n} must be a plain object`);let r=Object.getPrototypeOf(e);if(r!==Object.prototype&&r!==null)throw new WardConfigError(`${n} must be a plain object`);if(typeof e.action!=`string`||!e.action.trim())throw new WardConfigError(`${n}.action must be a non-empty string`);if(e.action.endsWith(`:`))throw new WardConfigError(`${n}.action '${e.action}' ends with ':': did you mean '${e.action}*'?`);if(typeof e.resource!=`string`||!e.resource.trim())throw new WardConfigError(`${n}.resource must be a non-empty string`);if(e.resource.endsWith(`:`))throw new WardConfigError(`${n}.resource '${e.resource}' ends with ':': did you mean '${e.resource}*'?`);if(e.effect!==`allow`&&e.effect!==`deny`)throw new WardConfigError(`${n}.effect must be "allow" or "deny"`);if(e.condition!==void 0&&typeof e.condition!=`function`)throw new WardConfigError(`${n}.condition must be a function`);if(e.roles!==void 0&&(!Array.isArray(e.roles)||e.roles.some(e=>typeof e!=`string`||!e.trim())))throw new WardConfigError(`${n}.roles must be an array of non-empty strings`);return Object.freeze({action:e.action,...e.attributes===void 0?{}:{attributes:snapshotAttributes(e.attributes,`${n}.attributes`)},...e.condition===void 0?{}:{condition:e.condition},effect:e.effect,resource:e.resource,...e.roles===void 0?{}:{roles:Object.freeze([...e.roles])}})}function matchesPattern(e,t){if(e===`*`||e===t)return!0;if(e.endsWith(`:*`)){let n=e.slice(0,-1);return t.startsWith(n)}return!1}function assertUserPrincipal(e){if(typeof e!=`object`||!e)throw new WardConfigError(`Invalid principal: expected { id: string, roles: string[] }`);let t=e;if(typeof t.id!=`string`||!t.id.trim())throw new WardConfigError(`Invalid principal: id must be a non-empty string`);if(!Array.isArray(t.roles)||t.roles.some(e=>typeof e!=`string`||!e.trim()))throw new WardConfigError(`Invalid principal: roles must be an array of non-empty strings`)}function snapshotPrincipal(e){return e==null?null:(assertUserPrincipal(e),Object.freeze({...e.attributes===void 0?{}:{attributes:snapshotAttributes(e.attributes,`Principal.attributes`)},id:e.id,roles:Object.freeze([...e.roles])}))}function snapshotDecisionInput(e){if(typeof e.action!=`string`||!e.action.trim())throw new WardConfigError(`Decision.action must be a non-empty string`);if(typeof e.resource!=`string`||!e.resource.trim())throw new WardConfigError(`Decision.resource must be a non-empty string`);return Object.freeze({action:e.action,...e.attributes===void 0?{}:{attributes:snapshotAttributes(e.attributes,`Decision.attributes`)},principal:snapshotPrincipal(e.principal),resource:e.resource})}function deepEqual(e,t){if(Object.is(e,t))return!0;if(typeof e!=`object`||typeof t!=`object`||e===null||t===null)return!1;if(Array.isArray(e)||Array.isArray(t))return Array.isArray(e)&&Array.isArray(t)&&e.length===t.length&&e.every((e,n)=>deepEqual(e,t[n]));let n=e,r=t,i=Object.keys(n),a=Object.keys(r);return i.length===a.length&&i.every(e=>Object.hasOwn(r,e)&&deepEqual(n[e],r[e]))}function attributesMatch(e,t){return e?t?Object.keys(e).every(n=>Object.hasOwn(t,n)&&deepEqual(e[n],t[n])):!1:!0}function rolesMatch(e,t){return!e||e.some(e=>e===`anonymous`?t===null:e===`*`?t!==null:t?.roles.includes(e)??!1)}function isThenable(e){return typeof e==`object`&&!!e&&`then`in e&&typeof e.then==`function`||typeof e==`function`&&`then`in e&&typeof e.then==`function`}function ruleMatches(e,t,n){if(!matchesPattern(e.action,n.action)||!matchesPattern(e.resource,n.resource)||!rolesMatch(e.roles,n.principal)||!attributesMatch(e.attributes,n.attributes))return!1;if(!e.condition)return!0;try{let t=e.condition({attributes:n.attributes,principal:n.principal});if(isThenable(t))throw TypeError(`Async conditions are not supported; return a boolean`);if(typeof t!=`boolean`)throw TypeError(`Conditions must return a boolean`);return t}catch(e){throw new WardConditionError(t,e)}}function createWard(e=[]){let n=Object.freeze(e.flat().map((e,t)=>compileRule(e,t))),r=Object.freeze([...new Set(n.flatMap(e=>e.effect===`allow`&&!e.action.includes(`*`)?[e.action]:[]))]),i=(0,t.tapper)();function evaluate(e,t){let r=snapshotDecisionInput(e);for(let e=0;e<n.length;e++){let i=n[e];if(ruleMatches(i,e,r)){let n=Object.freeze({effect:i.effect,matched:!0,reason:`rule[${e}] ${i.effect===`allow`?`allows`:`denies`} '${r.action}' on '${r.resource}'`,rule:i});return t&&emitDecision(r,n),n}}let i=Object.freeze({effect:`deny`,matched:!1,reason:`no matching rule for '${r.action}' on '${r.resource}' (default deny)`});return t&&emitDecision(r,i),i}function emitDecision(e,t){i.emit(Object.freeze({decision:t,input:e,type:`decision`}))}function decide(e){return evaluate(e,!0)}function checkAll(e){return e.map(decide)}function allowedActions(e){let t=e.filter??r,n=new Set;return t.filter(t=>!n.has(t)&&(n.add(t),evaluate({...e,action:t},!1).effect===`allow`))}function forPrincipal(e){let t=snapshotPrincipal(e);return{allowedActions:e=>allowedActions({...e,principal:t}),checkAll:e=>checkAll(e.map(e=>({...e,principal:t}))),decide:e=>decide({...e,principal:t})}}return{allowedActions,checkAll,decide,forPrincipal,knownActions:r,rules:n,tap:(e,t)=>i.tap(e,t)}}function buildRules(e,t,n,r,i){let a=Object.freeze(Array.isArray(t)?[...t]:[t]);return r.map(t=>({action:t,...i?.when===void 0?{}:{condition:i.when},effect:e,resource:n,roles:a}))}var allow=(e,t,n,r)=>buildRules(`allow`,e,t,n,r),deny=(e,t,n,r)=>buildRules(`deny`,e,t,n,r),owns=e=>({attributes:t,principal:n})=>n!==null&&t?.[e]===n.id,and=(...e)=>t=>e.every(e=>e(t)),or=(...e)=>t=>e.some(e=>e(t)),not=e=>t=>!e(t),i={and,not,or,owns};return e.ANONYMOUS=r,e.WILDCARD=n,e.WardConditionError=WardConditionError,e.WardConfigError=WardConfigError,e.WardError=WardError,e.allow=allow,e.createWard=createWard,e.deny=deny,e.matchesPattern=matchesPattern,e.predicate=i,e})({},Arsenal);
|
|
2
2
|
//# sourceMappingURL=ward.iife.js.map
|
package/dist/ward.iife.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ward.iife.js","names":[],"sources":["../src/constants.ts","../src/errors.ts","../src/_compile.ts","../src/resource.ts","../src/_match.ts","../src/factory.ts","../src/rules.ts"],"sourcesContent":["export const WILDCARD = '*' as const;\nexport const ANONYMOUS = 'anonymous' as const;\n","/** 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\n/** Thrown when a rule definition, decision attributes, or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/** Thrown when an application-provided rule condition fails. */\nexport class WardConditionError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n super(`Rule[${ruleIndex}].condition failed`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n","import { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':' — did you mean '${rule.action}*'?`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if (rule.resource.endsWith(':')) {\n throw new WardConfigError(`${at}.resource '${rule.resource}' ends with ':' — did you mean '${rule.resource}*'?`);\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.condition !== undefined && typeof rule.condition !== 'function') {\n throw new WardConfigError(`${at}.condition must be a function`);\n }\n\n if (rule.roles !== undefined) {\n if (!Array.isArray(rule.roles) || rule.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError(`${at}.roles must be an array of non-empty strings`);\n }\n }\n\n return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n ...(rule.roles === undefined ? {} : { roles: Object.freeze([...rule.roles]) }),\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n","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","import { snapshotAttributes } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConditionError, WardConfigError } from './errors';\nimport { matchesPattern } from './resource';\nimport type {\n Principal,\n UserPrincipal,\n WardAttributes,\n WardAttributeValue,\n WardDecisionInput,\n WardRule,\n} from './types';\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const principal = input as Record<string, unknown>;\n\n if (typeof principal.id !== 'string' || !principal.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(principal.roles) || principal.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function snapshotPrincipal(principal: Principal | undefined): Principal {\n if (principal === undefined || principal === null) return null;\n\n assertUserPrincipal(principal);\n\n return Object.freeze({\n ...(principal.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(principal.attributes, 'Principal.attributes') }),\n id: principal.id,\n roles: Object.freeze([...principal.roles]),\n });\n}\n\nexport function snapshotDecisionInput<\n TAction extends string,\n TResource extends string,\n TAttributes extends WardAttributes,\n>(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n): WardDecisionInput<TAction, TResource, TAttributes> & {\n principal: Principal;\n} {\n if (typeof input.action !== 'string' || !input.action.trim()) {\n throw new WardConfigError('Decision.action must be a non-empty string');\n }\n if (typeof input.resource !== 'string' || !input.resource.trim()) {\n throw new WardConfigError('Decision.resource must be a non-empty string');\n }\n\n return Object.freeze({\n action: input.action,\n ...(input.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(input.attributes, 'Decision.attributes') as TAttributes }),\n principal: snapshotPrincipal(input.principal),\n resource: input.resource,\n });\n}\n\nfunction deepEqual(left: WardAttributeValue, right: WardAttributeValue): boolean {\n if (Object.is(left, right)) return true;\n if (typeof left !== 'object' || typeof right !== 'object' || left === null || right === null) return false;\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((value, index) => deepEqual(value, right[index]))\n );\n }\n\n const leftRecord = left as Record<string, WardAttributeValue>;\n const rightRecord = right as Record<string, WardAttributeValue>;\n const leftKeys = Object.keys(leftRecord);\n const rightKeys = Object.keys(rightRecord);\n\n return (\n leftKeys.length === rightKeys.length &&\n leftKeys.every((key) => Object.hasOwn(rightRecord, key) && deepEqual(leftRecord[key], rightRecord[key]))\n );\n}\n\nexport function attributesMatch(expected: WardAttributes | undefined, actual: WardAttributes | undefined): boolean {\n if (!expected) return true;\n if (!actual) return false;\n\n return Object.keys(expected).every((key) => Object.hasOwn(actual, key) && deepEqual(expected[key], actual[key]));\n}\n\n/**\n * Declarative role check: `ANONYMOUS` matches a null principal, `WILDCARD`\n * matches any authenticated principal, an exact role must be in the principal's\n * roles. A rule without `roles` matches every principal.\n */\nfunction rolesMatch(expected: readonly string[] | undefined, principal: Principal): boolean {\n if (!expected) return true;\n\n return expected.some((candidate) =>\n candidate === ANONYMOUS\n ? principal === null\n : candidate === WILDCARD\n ? principal !== null\n : (principal?.roles.includes(candidate) ?? false),\n );\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n (typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function') ||\n (typeof value === 'function' && 'then' in value && typeof value.then === 'function')\n );\n}\n\nexport function ruleMatches<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n): boolean {\n if (!matchesPattern(rule.action, input.action)) return false;\n if (!matchesPattern(rule.resource, input.resource)) return false;\n if (!rolesMatch(rule.roles, input.principal)) return false;\n if (!attributesMatch(rule.attributes, input.attributes)) return false;\n if (!rule.condition) return true;\n\n try {\n const result: unknown = rule.condition({ attributes: input.attributes, principal: input.principal });\n\n if (isThenable(result)) {\n throw new TypeError('Async conditions are not supported; return a boolean');\n }\n if (typeof result !== 'boolean') {\n throw new TypeError('Conditions must return a boolean');\n }\n\n return result;\n } catch (error) {\n throw new WardConditionError(index, error);\n }\n}\n","import { tapper } from '@vielzeug/arsenal';\n\nimport { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport type {\n BoundWard,\n Principal,\n Ward,\n WardAttributes,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardRule,\n} from './types';\n\n/**\n * Creates an authorization ward from an ordered list of rules.\n *\n * **Decision model**: rules are evaluated in declaration order; the first rule\n * that matches the request wins. If no rule matches, the decision is **deny**\n * (default deny). There is no priority, specificity scoring, or conflict\n * detection — order is the only tiebreaker.\n *\n * A rule matches when:\n * 1. Its `action` pattern matches the requested action (`*` or `ns:*` or exact).\n * 2. Its `resource` pattern matches the requested resource.\n * 3. Its declarative `roles` (if any) include the principal's role, `ANONYMOUS`\n * matches a null principal, and `WILDCARD` matches any authenticated principal.\n * 4. Every key in its declarative `attributes` is present and equal in the\n * request's `attributes` (deep equality).\n * 5. Its `condition` escape-hatch callback (if any) returns `true`.\n */\nexport function createWard<\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n rules: readonly (\n | WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>\n | readonly WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>[]\n )[] = [],\n): Ward<TAction, TResource, TAttributes> {\n const compiled = Object.freeze(rules.flat().map((rule, index) => compileRule(rule, index)));\n const knownActions = Object.freeze([\n ...new Set(\n compiled.flatMap((rule) => (rule.effect === 'allow' && !rule.action.includes('*') ? [rule.action] : [])),\n ),\n ]) as readonly TAction[];\n const tappers = tapper<WardEvent<TAction, TResource, TAttributes>>();\n\n function evaluate(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n emit: boolean,\n ): WardDecision<TAction, TResource, TAttributes> {\n const normalized = snapshotDecisionInput(input);\n\n for (let index = 0; index < compiled.length; index++) {\n const rule = compiled[index];\n\n if (ruleMatches(rule, index, normalized)) {\n const decision = Object.freeze({\n effect: rule.effect,\n matched: true,\n reason: `rule[${index}] ${rule.effect === 'allow' ? 'allows' : 'denies'} '${normalized.action}' on '${normalized.resource}'`,\n rule,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n }\n\n const decision = Object.freeze({\n effect: 'deny',\n matched: false,\n reason: `no matching rule for '${normalized.action}' on '${normalized.resource}' (default deny)`,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n\n function emitDecision(\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n decision: WardDecision<TAction, TResource, TAttributes>,\n ): void {\n tappers.emit(Object.freeze({ decision, input, type: 'decision' as const }));\n }\n\n function decide(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n ): WardDecision<TAction, TResource, TAttributes> {\n return evaluate(input, true);\n }\n\n function checkAll(\n inputs: readonly WardDecisionInput<TAction, TResource, TAttributes>[],\n ): WardDecision<TAction, TResource, TAttributes>[] {\n return inputs.map(decide);\n }\n\n function allowedActions(input: {\n attributes?: TAttributes;\n filter?: readonly TAction[];\n principal?: Principal;\n resource: TResource;\n }): TAction[] {\n const candidates = input.filter ?? knownActions;\n const seen = new Set<TAction>();\n\n return candidates.filter((action) => {\n if (seen.has(action)) return false;\n seen.add(action);\n return evaluate({ ...input, action }, false).effect === 'allow';\n });\n }\n\n function forPrincipal(principal?: Principal): BoundWard<TAction, TResource, TAttributes> {\n const snapshot = snapshotPrincipal(principal);\n\n return {\n allowedActions: (input) => allowedActions({ ...input, principal: snapshot }),\n checkAll: (inputs) => checkAll(inputs.map((input) => ({ ...input, principal: snapshot }))),\n decide: (input) => decide({ ...input, principal: snapshot }),\n };\n }\n\n return {\n allowedActions,\n checkAll,\n decide,\n forPrincipal,\n knownActions,\n rules: compiled,\n tap: (handler, options) => tappers.tap(handler, options),\n };\n}\n","import type { WardAttributes, WardCondition, WardPattern, WardRule } from './types';\n\ntype RuleOptions<TAttributes extends WardAttributes> = Readonly<{\n when?: WardCondition<TAttributes>;\n}>;\n\nfunction buildRules<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] {\n const roles = Object.freeze(Array.isArray(role) ? [...role] : [role]);\n\n return actions.map((action) => ({\n action,\n ...(options?.when === undefined ? {} : { condition: options.when }),\n effect,\n resource,\n roles,\n }));\n}\n\nexport const allow = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('allow', role, resource, actions, options);\n\nexport const deny = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('deny', role, resource, actions, options);\n\nconst owns =\n <TAttributes extends WardAttributes = WardAttributes>(\n attribute: keyof TAttributes & string,\n ): WardCondition<TAttributes> =>\n ({ attributes, principal }) =>\n principal !== null && attributes?.[attribute] === principal.id;\nconst and =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.every((condition) => condition(input));\nconst or =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.some((condition) => condition(input));\nconst not =\n <TAttributes extends WardAttributes = WardAttributes>(\n condition: WardCondition<TAttributes>,\n ): WardCondition<TAttributes> =>\n (input) =>\n !condition(input);\n\nexport const predicate = { and, not, or, owns } as const;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ward.iife.js","names":[],"sources":["../src/constants.ts","../src/errors.ts","../src/_compile.ts","../src/resource.ts","../src/_match.ts","../src/factory.ts","../src/rules.ts"],"sourcesContent":["export const WILDCARD = '*' as const;\nexport const ANONYMOUS = 'anonymous' as const;\n","/** 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\n/** Thrown when a rule definition, decision attributes, or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/** Thrown when an application-provided rule condition fails. */\nexport class WardConditionError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n super(`Rule[${ruleIndex}].condition failed`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n","import { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':': did you mean '${rule.action}*'?`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if (rule.resource.endsWith(':')) {\n throw new WardConfigError(`${at}.resource '${rule.resource}' ends with ':': did you mean '${rule.resource}*'?`);\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.condition !== undefined && typeof rule.condition !== 'function') {\n throw new WardConfigError(`${at}.condition must be a function`);\n }\n\n if (rule.roles !== undefined) {\n if (!Array.isArray(rule.roles) || rule.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError(`${at}.roles must be an array of non-empty strings`);\n }\n }\n\n return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n ...(rule.roles === undefined ? {} : { roles: Object.freeze([...rule.roles]) }),\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n","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","import { snapshotAttributes } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConditionError, WardConfigError } from './errors';\nimport { matchesPattern } from './resource';\nimport type {\n Principal,\n UserPrincipal,\n WardAttributes,\n WardAttributeValue,\n WardDecisionInput,\n WardRule,\n} from './types';\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const principal = input as Record<string, unknown>;\n\n if (typeof principal.id !== 'string' || !principal.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(principal.roles) || principal.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function snapshotPrincipal(principal: Principal | undefined): Principal {\n if (principal === undefined || principal === null) return null;\n\n assertUserPrincipal(principal);\n\n return Object.freeze({\n ...(principal.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(principal.attributes, 'Principal.attributes') }),\n id: principal.id,\n roles: Object.freeze([...principal.roles]),\n });\n}\n\nexport function snapshotDecisionInput<\n TAction extends string,\n TResource extends string,\n TAttributes extends WardAttributes,\n>(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n): WardDecisionInput<TAction, TResource, TAttributes> & {\n principal: Principal;\n} {\n if (typeof input.action !== 'string' || !input.action.trim()) {\n throw new WardConfigError('Decision.action must be a non-empty string');\n }\n if (typeof input.resource !== 'string' || !input.resource.trim()) {\n throw new WardConfigError('Decision.resource must be a non-empty string');\n }\n\n return Object.freeze({\n action: input.action,\n ...(input.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(input.attributes, 'Decision.attributes') as TAttributes }),\n principal: snapshotPrincipal(input.principal),\n resource: input.resource,\n });\n}\n\nfunction deepEqual(left: WardAttributeValue, right: WardAttributeValue): boolean {\n if (Object.is(left, right)) return true;\n if (typeof left !== 'object' || typeof right !== 'object' || left === null || right === null) return false;\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((value, index) => deepEqual(value, right[index]))\n );\n }\n\n const leftRecord = left as Record<string, WardAttributeValue>;\n const rightRecord = right as Record<string, WardAttributeValue>;\n const leftKeys = Object.keys(leftRecord);\n const rightKeys = Object.keys(rightRecord);\n\n return (\n leftKeys.length === rightKeys.length &&\n leftKeys.every((key) => Object.hasOwn(rightRecord, key) && deepEqual(leftRecord[key], rightRecord[key]))\n );\n}\n\nexport function attributesMatch(expected: WardAttributes | undefined, actual: WardAttributes | undefined): boolean {\n if (!expected) return true;\n if (!actual) return false;\n\n return Object.keys(expected).every((key) => Object.hasOwn(actual, key) && deepEqual(expected[key], actual[key]));\n}\n\n/**\n * Declarative role check: `ANONYMOUS` matches a null principal, `WILDCARD`\n * matches any authenticated principal, an exact role must be in the principal's\n * roles. A rule without `roles` matches every principal.\n */\nfunction rolesMatch(expected: readonly string[] | undefined, principal: Principal): boolean {\n if (!expected) return true;\n\n return expected.some((candidate) =>\n candidate === ANONYMOUS\n ? principal === null\n : candidate === WILDCARD\n ? principal !== null\n : (principal?.roles.includes(candidate) ?? false),\n );\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n (typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function') ||\n (typeof value === 'function' && 'then' in value && typeof value.then === 'function')\n );\n}\n\nexport function ruleMatches<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n): boolean {\n if (!matchesPattern(rule.action, input.action)) return false;\n if (!matchesPattern(rule.resource, input.resource)) return false;\n if (!rolesMatch(rule.roles, input.principal)) return false;\n if (!attributesMatch(rule.attributes, input.attributes)) return false;\n if (!rule.condition) return true;\n\n try {\n const result: unknown = rule.condition({ attributes: input.attributes, principal: input.principal });\n\n if (isThenable(result)) {\n throw new TypeError('Async conditions are not supported; return a boolean');\n }\n if (typeof result !== 'boolean') {\n throw new TypeError('Conditions must return a boolean');\n }\n\n return result;\n } catch (error) {\n throw new WardConditionError(index, error);\n }\n}\n","import { tapper } from '@vielzeug/arsenal';\n\nimport { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport type {\n BoundWard,\n Principal,\n Ward,\n WardAttributes,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardRule,\n} from './types';\n\n/**\n * Creates an authorization ward from an ordered list of rules.\n *\n * **Decision model**: rules are evaluated in declaration order; the first rule\n * that matches the request wins. If no rule matches, the decision is **deny**\n * (default deny). There is no priority, specificity scoring, or conflict\n * detection: order is the only tiebreaker.\n *\n * A rule matches when:\n * 1. Its `action` pattern matches the requested action (`*` or `ns:*` or exact).\n * 2. Its `resource` pattern matches the requested resource.\n * 3. Its declarative `roles` (if any) include the principal's role, `ANONYMOUS`\n * matches a null principal, and `WILDCARD` matches any authenticated principal.\n * 4. Every key in its declarative `attributes` is present and equal in the\n * request's `attributes` (deep equality).\n * 5. Its `condition` escape-hatch callback (if any) returns `true`.\n */\nexport function createWard<\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n rules: readonly (\n | WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>\n | readonly WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>[]\n )[] = [],\n): Ward<TAction, TResource, TAttributes> {\n const compiled = Object.freeze(rules.flat().map((rule, index) => compileRule(rule, index)));\n const knownActions = Object.freeze([\n ...new Set(\n compiled.flatMap((rule) => (rule.effect === 'allow' && !rule.action.includes('*') ? [rule.action] : [])),\n ),\n ]) as readonly TAction[];\n const tappers = tapper<WardEvent<TAction, TResource, TAttributes>>();\n\n function evaluate(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n emit: boolean,\n ): WardDecision<TAction, TResource, TAttributes> {\n const normalized = snapshotDecisionInput(input);\n\n for (let index = 0; index < compiled.length; index++) {\n const rule = compiled[index];\n\n if (ruleMatches(rule, index, normalized)) {\n const decision = Object.freeze({\n effect: rule.effect,\n matched: true,\n reason: `rule[${index}] ${rule.effect === 'allow' ? 'allows' : 'denies'} '${normalized.action}' on '${normalized.resource}'`,\n rule,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n }\n\n const decision = Object.freeze({\n effect: 'deny',\n matched: false,\n reason: `no matching rule for '${normalized.action}' on '${normalized.resource}' (default deny)`,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n\n function emitDecision(\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n decision: WardDecision<TAction, TResource, TAttributes>,\n ): void {\n tappers.emit(Object.freeze({ decision, input, type: 'decision' as const }));\n }\n\n function decide(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n ): WardDecision<TAction, TResource, TAttributes> {\n return evaluate(input, true);\n }\n\n function checkAll(\n inputs: readonly WardDecisionInput<TAction, TResource, TAttributes>[],\n ): WardDecision<TAction, TResource, TAttributes>[] {\n return inputs.map(decide);\n }\n\n function allowedActions(input: {\n attributes?: TAttributes;\n filter?: readonly TAction[];\n principal?: Principal;\n resource: TResource;\n }): TAction[] {\n const candidates = input.filter ?? knownActions;\n const seen = new Set<TAction>();\n\n return candidates.filter((action) => {\n if (seen.has(action)) return false;\n seen.add(action);\n return evaluate({ ...input, action }, false).effect === 'allow';\n });\n }\n\n function forPrincipal(principal?: Principal): BoundWard<TAction, TResource, TAttributes> {\n const snapshot = snapshotPrincipal(principal);\n\n return {\n allowedActions: (input) => allowedActions({ ...input, principal: snapshot }),\n checkAll: (inputs) => checkAll(inputs.map((input) => ({ ...input, principal: snapshot }))),\n decide: (input) => decide({ ...input, principal: snapshot }),\n };\n }\n\n return {\n allowedActions,\n checkAll,\n decide,\n forPrincipal,\n knownActions,\n rules: compiled,\n tap: (handler, options) => tappers.tap(handler, options),\n };\n}\n","import type { WardAttributes, WardCondition, WardPattern, WardRule } from './types';\n\ntype RuleOptions<TAttributes extends WardAttributes> = Readonly<{\n when?: WardCondition<TAttributes>;\n}>;\n\nfunction buildRules<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] {\n const roles = Object.freeze(Array.isArray(role) ? [...role] : [role]);\n\n return actions.map((action) => ({\n action,\n ...(options?.when === undefined ? {} : { condition: options.when }),\n effect,\n resource,\n roles,\n }));\n}\n\nexport const allow = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('allow', role, resource, actions, options);\n\nexport const deny = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('deny', role, resource, actions, options);\n\nconst owns =\n <TAttributes extends WardAttributes = WardAttributes>(\n attribute: keyof TAttributes & string,\n ): WardCondition<TAttributes> =>\n ({ attributes, principal }) =>\n principal !== null && attributes?.[attribute] === principal.id;\nconst and =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.every((condition) => condition(input));\nconst or =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.some((condition) => condition(input));\nconst not =\n <TAttributes extends WardAttributes = WardAttributes>(\n condition: WardCondition<TAttributes>,\n ): WardCondition<TAttributes> =>\n (input) =>\n !condition(input);\n\nexport const predicate = { and, not, or, owns } as const;\n"],"mappings":"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"}
|
package/dist/ward.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{tapper as e}from"@vielzeug/arsenal";var t=`*`,n=`anonymous`,WardError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},WardConfigError=class extends WardError{},WardConditionError=class extends WardError{ruleIndex;constructor(e,t){super(`Rule[${e}].condition failed`,{cause:t}),this.ruleIndex=e}};function snapshotValue(e,t,n){if(e===null||typeof e==`boolean`||typeof e==`string`)return e;if(typeof e==`number`){if(!Number.isFinite(e))throw new WardConfigError(`${t} must contain only finite numbers`);return e}if(typeof e!=`object`)throw new WardConfigError(`${t} must contain only JSON-compatible values`);if(n.has(e))throw new WardConfigError(`${t} must not contain circular references`);let r=Object.getPrototypeOf(e);if(!Array.isArray(e)&&r!==Object.prototype&&r!==null)throw new WardConfigError(`${t} must contain only arrays and plain objects`);n.add(e);try{return Object.freeze(Array.isArray(e)?Array.from(e,(e,r)=>snapshotValue(e,`${t}[${r}]`,n)):Object.fromEntries(Object.keys(e).map(r=>[r,snapshotValue(e[r],`${t}.${r}`,n)])))}finally{n.delete(e)}}function snapshotAttributes(e,t){if(e!==void 0){if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${t} must be a plain object`);return snapshotValue(e,t,new Set)}}function compileRule(e,t){let n=`Rule[${t}]`;if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${n} must be a plain object`);let r=Object.getPrototypeOf(e);if(r!==Object.prototype&&r!==null)throw new WardConfigError(`${n} must be a plain object`);if(typeof e.action!=`string`||!e.action.trim())throw new WardConfigError(`${n}.action must be a non-empty string`);if(e.action.endsWith(`:`))throw new WardConfigError(`${n}.action '${e.action}' ends with ':'
|
|
1
|
+
import{tapper as e}from"@vielzeug/arsenal";var t=`*`,n=`anonymous`,WardError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},WardConfigError=class extends WardError{},WardConditionError=class extends WardError{ruleIndex;constructor(e,t){super(`Rule[${e}].condition failed`,{cause:t}),this.ruleIndex=e}};function snapshotValue(e,t,n){if(e===null||typeof e==`boolean`||typeof e==`string`)return e;if(typeof e==`number`){if(!Number.isFinite(e))throw new WardConfigError(`${t} must contain only finite numbers`);return e}if(typeof e!=`object`)throw new WardConfigError(`${t} must contain only JSON-compatible values`);if(n.has(e))throw new WardConfigError(`${t} must not contain circular references`);let r=Object.getPrototypeOf(e);if(!Array.isArray(e)&&r!==Object.prototype&&r!==null)throw new WardConfigError(`${t} must contain only arrays and plain objects`);n.add(e);try{return Object.freeze(Array.isArray(e)?Array.from(e,(e,r)=>snapshotValue(e,`${t}[${r}]`,n)):Object.fromEntries(Object.keys(e).map(r=>[r,snapshotValue(e[r],`${t}.${r}`,n)])))}finally{n.delete(e)}}function snapshotAttributes(e,t){if(e!==void 0){if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${t} must be a plain object`);return snapshotValue(e,t,new Set)}}function compileRule(e,t){let n=`Rule[${t}]`;if(typeof e!=`object`||!e||Array.isArray(e))throw new WardConfigError(`${n} must be a plain object`);let r=Object.getPrototypeOf(e);if(r!==Object.prototype&&r!==null)throw new WardConfigError(`${n} must be a plain object`);if(typeof e.action!=`string`||!e.action.trim())throw new WardConfigError(`${n}.action must be a non-empty string`);if(e.action.endsWith(`:`))throw new WardConfigError(`${n}.action '${e.action}' ends with ':': did you mean '${e.action}*'?`);if(typeof e.resource!=`string`||!e.resource.trim())throw new WardConfigError(`${n}.resource must be a non-empty string`);if(e.resource.endsWith(`:`))throw new WardConfigError(`${n}.resource '${e.resource}' ends with ':': did you mean '${e.resource}*'?`);if(e.effect!==`allow`&&e.effect!==`deny`)throw new WardConfigError(`${n}.effect must be "allow" or "deny"`);if(e.condition!==void 0&&typeof e.condition!=`function`)throw new WardConfigError(`${n}.condition must be a function`);if(e.roles!==void 0&&(!Array.isArray(e.roles)||e.roles.some(e=>typeof e!=`string`||!e.trim())))throw new WardConfigError(`${n}.roles must be an array of non-empty strings`);return Object.freeze({action:e.action,...e.attributes===void 0?{}:{attributes:snapshotAttributes(e.attributes,`${n}.attributes`)},...e.condition===void 0?{}:{condition:e.condition},effect:e.effect,resource:e.resource,...e.roles===void 0?{}:{roles:Object.freeze([...e.roles])}})}function matchesPattern(e,t){if(e===`*`||e===t)return!0;if(e.endsWith(`:*`)){let n=e.slice(0,-1);return t.startsWith(n)}return!1}function assertUserPrincipal(e){if(typeof e!=`object`||!e)throw new WardConfigError(`Invalid principal: expected { id: string, roles: string[] }`);let t=e;if(typeof t.id!=`string`||!t.id.trim())throw new WardConfigError(`Invalid principal: id must be a non-empty string`);if(!Array.isArray(t.roles)||t.roles.some(e=>typeof e!=`string`||!e.trim()))throw new WardConfigError(`Invalid principal: roles must be an array of non-empty strings`)}function snapshotPrincipal(e){return e==null?null:(assertUserPrincipal(e),Object.freeze({...e.attributes===void 0?{}:{attributes:snapshotAttributes(e.attributes,`Principal.attributes`)},id:e.id,roles:Object.freeze([...e.roles])}))}function snapshotDecisionInput(e){if(typeof e.action!=`string`||!e.action.trim())throw new WardConfigError(`Decision.action must be a non-empty string`);if(typeof e.resource!=`string`||!e.resource.trim())throw new WardConfigError(`Decision.resource must be a non-empty string`);return Object.freeze({action:e.action,...e.attributes===void 0?{}:{attributes:snapshotAttributes(e.attributes,`Decision.attributes`)},principal:snapshotPrincipal(e.principal),resource:e.resource})}function deepEqual(e,t){if(Object.is(e,t))return!0;if(typeof e!=`object`||typeof t!=`object`||e===null||t===null)return!1;if(Array.isArray(e)||Array.isArray(t))return Array.isArray(e)&&Array.isArray(t)&&e.length===t.length&&e.every((e,n)=>deepEqual(e,t[n]));let n=e,r=t,i=Object.keys(n),a=Object.keys(r);return i.length===a.length&&i.every(e=>Object.hasOwn(r,e)&&deepEqual(n[e],r[e]))}function attributesMatch(e,t){return e?t?Object.keys(e).every(n=>Object.hasOwn(t,n)&&deepEqual(e[n],t[n])):!1:!0}function rolesMatch(e,t){return!e||e.some(e=>e===`anonymous`?t===null:e===`*`?t!==null:t?.roles.includes(e)??!1)}function isThenable(e){return typeof e==`object`&&!!e&&`then`in e&&typeof e.then==`function`||typeof e==`function`&&`then`in e&&typeof e.then==`function`}function ruleMatches(e,t,n){if(!matchesPattern(e.action,n.action)||!matchesPattern(e.resource,n.resource)||!rolesMatch(e.roles,n.principal)||!attributesMatch(e.attributes,n.attributes))return!1;if(!e.condition)return!0;try{let t=e.condition({attributes:n.attributes,principal:n.principal});if(isThenable(t))throw TypeError(`Async conditions are not supported; return a boolean`);if(typeof t!=`boolean`)throw TypeError(`Conditions must return a boolean`);return t}catch(e){throw new WardConditionError(t,e)}}function createWard(t=[]){let n=Object.freeze(t.flat().map((e,t)=>compileRule(e,t))),r=Object.freeze([...new Set(n.flatMap(e=>e.effect===`allow`&&!e.action.includes(`*`)?[e.action]:[]))]),i=e();function evaluate(e,t){let r=snapshotDecisionInput(e);for(let e=0;e<n.length;e++){let i=n[e];if(ruleMatches(i,e,r)){let n=Object.freeze({effect:i.effect,matched:!0,reason:`rule[${e}] ${i.effect===`allow`?`allows`:`denies`} '${r.action}' on '${r.resource}'`,rule:i});return t&&emitDecision(r,n),n}}let i=Object.freeze({effect:`deny`,matched:!1,reason:`no matching rule for '${r.action}' on '${r.resource}' (default deny)`});return t&&emitDecision(r,i),i}function emitDecision(e,t){i.emit(Object.freeze({decision:t,input:e,type:`decision`}))}function decide(e){return evaluate(e,!0)}function checkAll(e){return e.map(decide)}function allowedActions(e){let t=e.filter??r,n=new Set;return t.filter(t=>!n.has(t)&&(n.add(t),evaluate({...e,action:t},!1).effect===`allow`))}function forPrincipal(e){let t=snapshotPrincipal(e);return{allowedActions:e=>allowedActions({...e,principal:t}),checkAll:e=>checkAll(e.map(e=>({...e,principal:t}))),decide:e=>decide({...e,principal:t})}}return{allowedActions,checkAll,decide,forPrincipal,knownActions:r,rules:n,tap:(e,t)=>i.tap(e,t)}}function buildRules(e,t,n,r,i){let a=Object.freeze(Array.isArray(t)?[...t]:[t]);return r.map(t=>({action:t,...i?.when===void 0?{}:{condition:i.when},effect:e,resource:n,roles:a}))}var allow=(e,t,n,r)=>buildRules(`allow`,e,t,n,r),deny=(e,t,n,r)=>buildRules(`deny`,e,t,n,r),owns=e=>({attributes:t,principal:n})=>n!==null&&t?.[e]===n.id,and=(...e)=>t=>e.every(e=>e(t)),or=(...e)=>t=>e.some(e=>e(t)),not=e=>t=>!e(t),r={and,not,or,owns};export{n as ANONYMOUS,t as WILDCARD,WardConditionError,WardConfigError,WardError,allow,createWard,deny,matchesPattern,r as predicate};
|
|
2
2
|
//# sourceMappingURL=ward.js.map
|
package/dist/ward.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ward.js","names":[],"sources":["../src/constants.ts","../src/errors.ts","../src/_compile.ts","../src/resource.ts","../src/_match.ts","../src/factory.ts","../src/rules.ts"],"sourcesContent":["export const WILDCARD = '*' as const;\nexport const ANONYMOUS = 'anonymous' as const;\n","/** 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\n/** Thrown when a rule definition, decision attributes, or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/** Thrown when an application-provided rule condition fails. */\nexport class WardConditionError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n super(`Rule[${ruleIndex}].condition failed`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n","import { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':' — did you mean '${rule.action}*'?`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if (rule.resource.endsWith(':')) {\n throw new WardConfigError(`${at}.resource '${rule.resource}' ends with ':' — did you mean '${rule.resource}*'?`);\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.condition !== undefined && typeof rule.condition !== 'function') {\n throw new WardConfigError(`${at}.condition must be a function`);\n }\n\n if (rule.roles !== undefined) {\n if (!Array.isArray(rule.roles) || rule.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError(`${at}.roles must be an array of non-empty strings`);\n }\n }\n\n return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n ...(rule.roles === undefined ? {} : { roles: Object.freeze([...rule.roles]) }),\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n","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","import { snapshotAttributes } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConditionError, WardConfigError } from './errors';\nimport { matchesPattern } from './resource';\nimport type {\n Principal,\n UserPrincipal,\n WardAttributes,\n WardAttributeValue,\n WardDecisionInput,\n WardRule,\n} from './types';\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const principal = input as Record<string, unknown>;\n\n if (typeof principal.id !== 'string' || !principal.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(principal.roles) || principal.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function snapshotPrincipal(principal: Principal | undefined): Principal {\n if (principal === undefined || principal === null) return null;\n\n assertUserPrincipal(principal);\n\n return Object.freeze({\n ...(principal.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(principal.attributes, 'Principal.attributes') }),\n id: principal.id,\n roles: Object.freeze([...principal.roles]),\n });\n}\n\nexport function snapshotDecisionInput<\n TAction extends string,\n TResource extends string,\n TAttributes extends WardAttributes,\n>(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n): WardDecisionInput<TAction, TResource, TAttributes> & {\n principal: Principal;\n} {\n if (typeof input.action !== 'string' || !input.action.trim()) {\n throw new WardConfigError('Decision.action must be a non-empty string');\n }\n if (typeof input.resource !== 'string' || !input.resource.trim()) {\n throw new WardConfigError('Decision.resource must be a non-empty string');\n }\n\n return Object.freeze({\n action: input.action,\n ...(input.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(input.attributes, 'Decision.attributes') as TAttributes }),\n principal: snapshotPrincipal(input.principal),\n resource: input.resource,\n });\n}\n\nfunction deepEqual(left: WardAttributeValue, right: WardAttributeValue): boolean {\n if (Object.is(left, right)) return true;\n if (typeof left !== 'object' || typeof right !== 'object' || left === null || right === null) return false;\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((value, index) => deepEqual(value, right[index]))\n );\n }\n\n const leftRecord = left as Record<string, WardAttributeValue>;\n const rightRecord = right as Record<string, WardAttributeValue>;\n const leftKeys = Object.keys(leftRecord);\n const rightKeys = Object.keys(rightRecord);\n\n return (\n leftKeys.length === rightKeys.length &&\n leftKeys.every((key) => Object.hasOwn(rightRecord, key) && deepEqual(leftRecord[key], rightRecord[key]))\n );\n}\n\nexport function attributesMatch(expected: WardAttributes | undefined, actual: WardAttributes | undefined): boolean {\n if (!expected) return true;\n if (!actual) return false;\n\n return Object.keys(expected).every((key) => Object.hasOwn(actual, key) && deepEqual(expected[key], actual[key]));\n}\n\n/**\n * Declarative role check: `ANONYMOUS` matches a null principal, `WILDCARD`\n * matches any authenticated principal, an exact role must be in the principal's\n * roles. A rule without `roles` matches every principal.\n */\nfunction rolesMatch(expected: readonly string[] | undefined, principal: Principal): boolean {\n if (!expected) return true;\n\n return expected.some((candidate) =>\n candidate === ANONYMOUS\n ? principal === null\n : candidate === WILDCARD\n ? principal !== null\n : (principal?.roles.includes(candidate) ?? false),\n );\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n (typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function') ||\n (typeof value === 'function' && 'then' in value && typeof value.then === 'function')\n );\n}\n\nexport function ruleMatches<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n): boolean {\n if (!matchesPattern(rule.action, input.action)) return false;\n if (!matchesPattern(rule.resource, input.resource)) return false;\n if (!rolesMatch(rule.roles, input.principal)) return false;\n if (!attributesMatch(rule.attributes, input.attributes)) return false;\n if (!rule.condition) return true;\n\n try {\n const result: unknown = rule.condition({ attributes: input.attributes, principal: input.principal });\n\n if (isThenable(result)) {\n throw new TypeError('Async conditions are not supported; return a boolean');\n }\n if (typeof result !== 'boolean') {\n throw new TypeError('Conditions must return a boolean');\n }\n\n return result;\n } catch (error) {\n throw new WardConditionError(index, error);\n }\n}\n","import { tapper } from '@vielzeug/arsenal';\n\nimport { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport type {\n BoundWard,\n Principal,\n Ward,\n WardAttributes,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardRule,\n} from './types';\n\n/**\n * Creates an authorization ward from an ordered list of rules.\n *\n * **Decision model**: rules are evaluated in declaration order; the first rule\n * that matches the request wins. If no rule matches, the decision is **deny**\n * (default deny). There is no priority, specificity scoring, or conflict\n * detection — order is the only tiebreaker.\n *\n * A rule matches when:\n * 1. Its `action` pattern matches the requested action (`*` or `ns:*` or exact).\n * 2. Its `resource` pattern matches the requested resource.\n * 3. Its declarative `roles` (if any) include the principal's role, `ANONYMOUS`\n * matches a null principal, and `WILDCARD` matches any authenticated principal.\n * 4. Every key in its declarative `attributes` is present and equal in the\n * request's `attributes` (deep equality).\n * 5. Its `condition` escape-hatch callback (if any) returns `true`.\n */\nexport function createWard<\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n rules: readonly (\n | WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>\n | readonly WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>[]\n )[] = [],\n): Ward<TAction, TResource, TAttributes> {\n const compiled = Object.freeze(rules.flat().map((rule, index) => compileRule(rule, index)));\n const knownActions = Object.freeze([\n ...new Set(\n compiled.flatMap((rule) => (rule.effect === 'allow' && !rule.action.includes('*') ? [rule.action] : [])),\n ),\n ]) as readonly TAction[];\n const tappers = tapper<WardEvent<TAction, TResource, TAttributes>>();\n\n function evaluate(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n emit: boolean,\n ): WardDecision<TAction, TResource, TAttributes> {\n const normalized = snapshotDecisionInput(input);\n\n for (let index = 0; index < compiled.length; index++) {\n const rule = compiled[index];\n\n if (ruleMatches(rule, index, normalized)) {\n const decision = Object.freeze({\n effect: rule.effect,\n matched: true,\n reason: `rule[${index}] ${rule.effect === 'allow' ? 'allows' : 'denies'} '${normalized.action}' on '${normalized.resource}'`,\n rule,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n }\n\n const decision = Object.freeze({\n effect: 'deny',\n matched: false,\n reason: `no matching rule for '${normalized.action}' on '${normalized.resource}' (default deny)`,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n\n function emitDecision(\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n decision: WardDecision<TAction, TResource, TAttributes>,\n ): void {\n tappers.emit(Object.freeze({ decision, input, type: 'decision' as const }));\n }\n\n function decide(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n ): WardDecision<TAction, TResource, TAttributes> {\n return evaluate(input, true);\n }\n\n function checkAll(\n inputs: readonly WardDecisionInput<TAction, TResource, TAttributes>[],\n ): WardDecision<TAction, TResource, TAttributes>[] {\n return inputs.map(decide);\n }\n\n function allowedActions(input: {\n attributes?: TAttributes;\n filter?: readonly TAction[];\n principal?: Principal;\n resource: TResource;\n }): TAction[] {\n const candidates = input.filter ?? knownActions;\n const seen = new Set<TAction>();\n\n return candidates.filter((action) => {\n if (seen.has(action)) return false;\n seen.add(action);\n return evaluate({ ...input, action }, false).effect === 'allow';\n });\n }\n\n function forPrincipal(principal?: Principal): BoundWard<TAction, TResource, TAttributes> {\n const snapshot = snapshotPrincipal(principal);\n\n return {\n allowedActions: (input) => allowedActions({ ...input, principal: snapshot }),\n checkAll: (inputs) => checkAll(inputs.map((input) => ({ ...input, principal: snapshot }))),\n decide: (input) => decide({ ...input, principal: snapshot }),\n };\n }\n\n return {\n allowedActions,\n checkAll,\n decide,\n forPrincipal,\n knownActions,\n rules: compiled,\n tap: (handler, options) => tappers.tap(handler, options),\n };\n}\n","import type { WardAttributes, WardCondition, WardPattern, WardRule } from './types';\n\ntype RuleOptions<TAttributes extends WardAttributes> = Readonly<{\n when?: WardCondition<TAttributes>;\n}>;\n\nfunction buildRules<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] {\n const roles = Object.freeze(Array.isArray(role) ? [...role] : [role]);\n\n return actions.map((action) => ({\n action,\n ...(options?.when === undefined ? {} : { condition: options.when }),\n effect,\n resource,\n roles,\n }));\n}\n\nexport const allow = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('allow', role, resource, actions, options);\n\nexport const deny = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('deny', role, resource, actions, options);\n\nconst owns =\n <TAttributes extends WardAttributes = WardAttributes>(\n attribute: keyof TAttributes & string,\n ): WardCondition<TAttributes> =>\n ({ attributes, principal }) =>\n principal !== null && attributes?.[attribute] === principal.id;\nconst and =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.every((condition) => condition(input));\nconst or =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.some((condition) => condition(input));\nconst not =\n <TAttributes extends WardAttributes = WardAttributes>(\n condition: WardCondition<TAttributes>,\n ): WardCondition<TAttributes> =>\n (input) =>\n !condition(input);\n\nexport const predicate = { and, not, or, owns } as const;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ward.js","names":[],"sources":["../src/constants.ts","../src/errors.ts","../src/_compile.ts","../src/resource.ts","../src/_match.ts","../src/factory.ts","../src/rules.ts"],"sourcesContent":["export const WILDCARD = '*' as const;\nexport const ANONYMOUS = 'anonymous' as const;\n","/** 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\n/** Thrown when a rule definition, decision attributes, or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/** Thrown when an application-provided rule condition fails. */\nexport class WardConditionError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n super(`Rule[${ruleIndex}].condition failed`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n","import { WardConfigError } from './errors';\nimport type { WardAttributes, WardAttributeValue, WardRule } from './types';\n\nfunction snapshotValue(value: unknown, path: string, ancestors: Set<object>): WardAttributeValue {\n if (value === null || typeof value === 'boolean' || typeof value === 'string') return value;\n\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) throw new WardConfigError(`${path} must contain only finite numbers`);\n return value;\n }\n\n if (typeof value !== 'object') {\n throw new WardConfigError(`${path} must contain only JSON-compatible values`);\n }\n\n if (ancestors.has(value)) throw new WardConfigError(`${path} must not contain circular references`);\n\n const prototype = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${path} must contain only arrays and plain objects`);\n }\n\n ancestors.add(value);\n\n try {\n if (Array.isArray(value)) {\n return Object.freeze(Array.from(value, (item, index) => snapshotValue(item, `${path}[${index}]`, ancestors)));\n }\n\n return Object.freeze(\n Object.fromEntries(\n Object.keys(value).map((key) => [\n key,\n snapshotValue((value as Record<string, unknown>)[key], `${path}.${key}`, ancestors),\n ]),\n ),\n );\n } finally {\n ancestors.delete(value);\n }\n}\n\nexport function snapshotAttributes(value: unknown, path: string): WardAttributes | undefined {\n if (value === undefined) return undefined;\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n throw new WardConfigError(`${path} must be a plain object`);\n }\n\n return snapshotValue(value, path, new Set()) as WardAttributes;\n}\n\n/**\n * Validates and snapshots a single rule definition at ward-creation time.\n * Throws {@link WardConfigError} with a `Rule[index]`-prefixed message on failure.\n */\nexport function compileRule<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n): WardRule<TAction, TResource, TAttributes> {\n const at = `Rule[${index}]`;\n\n if (typeof rule !== 'object' || rule === null || Array.isArray(rule)) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n const prototype = Object.getPrototypeOf(rule);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new WardConfigError(`${at} must be a plain object`);\n }\n\n if (typeof rule.action !== 'string' || !rule.action.trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if (rule.action.endsWith(':')) {\n throw new WardConfigError(`${at}.action '${rule.action}' ends with ':': did you mean '${rule.action}*'?`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if (rule.resource.endsWith(':')) {\n throw new WardConfigError(`${at}.resource '${rule.resource}' ends with ':': did you mean '${rule.resource}*'?`);\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.condition !== undefined && typeof rule.condition !== 'function') {\n throw new WardConfigError(`${at}.condition must be a function`);\n }\n\n if (rule.roles !== undefined) {\n if (!Array.isArray(rule.roles) || rule.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError(`${at}.roles must be an array of non-empty strings`);\n }\n }\n\n return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n ...(rule.roles === undefined ? {} : { roles: Object.freeze([...rule.roles]) }),\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n","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","import { snapshotAttributes } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConditionError, WardConfigError } from './errors';\nimport { matchesPattern } from './resource';\nimport type {\n Principal,\n UserPrincipal,\n WardAttributes,\n WardAttributeValue,\n WardDecisionInput,\n WardRule,\n} from './types';\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const principal = input as Record<string, unknown>;\n\n if (typeof principal.id !== 'string' || !principal.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(principal.roles) || principal.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function snapshotPrincipal(principal: Principal | undefined): Principal {\n if (principal === undefined || principal === null) return null;\n\n assertUserPrincipal(principal);\n\n return Object.freeze({\n ...(principal.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(principal.attributes, 'Principal.attributes') }),\n id: principal.id,\n roles: Object.freeze([...principal.roles]),\n });\n}\n\nexport function snapshotDecisionInput<\n TAction extends string,\n TResource extends string,\n TAttributes extends WardAttributes,\n>(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n): WardDecisionInput<TAction, TResource, TAttributes> & {\n principal: Principal;\n} {\n if (typeof input.action !== 'string' || !input.action.trim()) {\n throw new WardConfigError('Decision.action must be a non-empty string');\n }\n if (typeof input.resource !== 'string' || !input.resource.trim()) {\n throw new WardConfigError('Decision.resource must be a non-empty string');\n }\n\n return Object.freeze({\n action: input.action,\n ...(input.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(input.attributes, 'Decision.attributes') as TAttributes }),\n principal: snapshotPrincipal(input.principal),\n resource: input.resource,\n });\n}\n\nfunction deepEqual(left: WardAttributeValue, right: WardAttributeValue): boolean {\n if (Object.is(left, right)) return true;\n if (typeof left !== 'object' || typeof right !== 'object' || left === null || right === null) return false;\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((value, index) => deepEqual(value, right[index]))\n );\n }\n\n const leftRecord = left as Record<string, WardAttributeValue>;\n const rightRecord = right as Record<string, WardAttributeValue>;\n const leftKeys = Object.keys(leftRecord);\n const rightKeys = Object.keys(rightRecord);\n\n return (\n leftKeys.length === rightKeys.length &&\n leftKeys.every((key) => Object.hasOwn(rightRecord, key) && deepEqual(leftRecord[key], rightRecord[key]))\n );\n}\n\nexport function attributesMatch(expected: WardAttributes | undefined, actual: WardAttributes | undefined): boolean {\n if (!expected) return true;\n if (!actual) return false;\n\n return Object.keys(expected).every((key) => Object.hasOwn(actual, key) && deepEqual(expected[key], actual[key]));\n}\n\n/**\n * Declarative role check: `ANONYMOUS` matches a null principal, `WILDCARD`\n * matches any authenticated principal, an exact role must be in the principal's\n * roles. A rule without `roles` matches every principal.\n */\nfunction rolesMatch(expected: readonly string[] | undefined, principal: Principal): boolean {\n if (!expected) return true;\n\n return expected.some((candidate) =>\n candidate === ANONYMOUS\n ? principal === null\n : candidate === WILDCARD\n ? principal !== null\n : (principal?.roles.includes(candidate) ?? false),\n );\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n (typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function') ||\n (typeof value === 'function' && 'then' in value && typeof value.then === 'function')\n );\n}\n\nexport function ruleMatches<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n): boolean {\n if (!matchesPattern(rule.action, input.action)) return false;\n if (!matchesPattern(rule.resource, input.resource)) return false;\n if (!rolesMatch(rule.roles, input.principal)) return false;\n if (!attributesMatch(rule.attributes, input.attributes)) return false;\n if (!rule.condition) return true;\n\n try {\n const result: unknown = rule.condition({ attributes: input.attributes, principal: input.principal });\n\n if (isThenable(result)) {\n throw new TypeError('Async conditions are not supported; return a boolean');\n }\n if (typeof result !== 'boolean') {\n throw new TypeError('Conditions must return a boolean');\n }\n\n return result;\n } catch (error) {\n throw new WardConditionError(index, error);\n }\n}\n","import { tapper } from '@vielzeug/arsenal';\n\nimport { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport type {\n BoundWard,\n Principal,\n Ward,\n WardAttributes,\n WardDecision,\n WardDecisionInput,\n WardEvent,\n WardRule,\n} from './types';\n\n/**\n * Creates an authorization ward from an ordered list of rules.\n *\n * **Decision model**: rules are evaluated in declaration order; the first rule\n * that matches the request wins. If no rule matches, the decision is **deny**\n * (default deny). There is no priority, specificity scoring, or conflict\n * detection: order is the only tiebreaker.\n *\n * A rule matches when:\n * 1. Its `action` pattern matches the requested action (`*` or `ns:*` or exact).\n * 2. Its `resource` pattern matches the requested resource.\n * 3. Its declarative `roles` (if any) include the principal's role, `ANONYMOUS`\n * matches a null principal, and `WILDCARD` matches any authenticated principal.\n * 4. Every key in its declarative `attributes` is present and equal in the\n * request's `attributes` (deep equality).\n * 5. Its `condition` escape-hatch callback (if any) returns `true`.\n */\nexport function createWard<\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n rules: readonly (\n | WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>\n | readonly WardRule<NoInfer<TAction>, NoInfer<TResource>, NoInfer<TAttributes>>[]\n )[] = [],\n): Ward<TAction, TResource, TAttributes> {\n const compiled = Object.freeze(rules.flat().map((rule, index) => compileRule(rule, index)));\n const knownActions = Object.freeze([\n ...new Set(\n compiled.flatMap((rule) => (rule.effect === 'allow' && !rule.action.includes('*') ? [rule.action] : [])),\n ),\n ]) as readonly TAction[];\n const tappers = tapper<WardEvent<TAction, TResource, TAttributes>>();\n\n function evaluate(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n emit: boolean,\n ): WardDecision<TAction, TResource, TAttributes> {\n const normalized = snapshotDecisionInput(input);\n\n for (let index = 0; index < compiled.length; index++) {\n const rule = compiled[index];\n\n if (ruleMatches(rule, index, normalized)) {\n const decision = Object.freeze({\n effect: rule.effect,\n matched: true,\n reason: `rule[${index}] ${rule.effect === 'allow' ? 'allows' : 'denies'} '${normalized.action}' on '${normalized.resource}'`,\n rule,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n }\n\n const decision = Object.freeze({\n effect: 'deny',\n matched: false,\n reason: `no matching rule for '${normalized.action}' on '${normalized.resource}' (default deny)`,\n }) as WardDecision<TAction, TResource, TAttributes>;\n\n if (emit) emitDecision(normalized, decision);\n return decision;\n }\n\n function emitDecision(\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n decision: WardDecision<TAction, TResource, TAttributes>,\n ): void {\n tappers.emit(Object.freeze({ decision, input, type: 'decision' as const }));\n }\n\n function decide(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n ): WardDecision<TAction, TResource, TAttributes> {\n return evaluate(input, true);\n }\n\n function checkAll(\n inputs: readonly WardDecisionInput<TAction, TResource, TAttributes>[],\n ): WardDecision<TAction, TResource, TAttributes>[] {\n return inputs.map(decide);\n }\n\n function allowedActions(input: {\n attributes?: TAttributes;\n filter?: readonly TAction[];\n principal?: Principal;\n resource: TResource;\n }): TAction[] {\n const candidates = input.filter ?? knownActions;\n const seen = new Set<TAction>();\n\n return candidates.filter((action) => {\n if (seen.has(action)) return false;\n seen.add(action);\n return evaluate({ ...input, action }, false).effect === 'allow';\n });\n }\n\n function forPrincipal(principal?: Principal): BoundWard<TAction, TResource, TAttributes> {\n const snapshot = snapshotPrincipal(principal);\n\n return {\n allowedActions: (input) => allowedActions({ ...input, principal: snapshot }),\n checkAll: (inputs) => checkAll(inputs.map((input) => ({ ...input, principal: snapshot }))),\n decide: (input) => decide({ ...input, principal: snapshot }),\n };\n }\n\n return {\n allowedActions,\n checkAll,\n decide,\n forPrincipal,\n knownActions,\n rules: compiled,\n tap: (handler, options) => tappers.tap(handler, options),\n };\n}\n","import type { WardAttributes, WardCondition, WardPattern, WardRule } from './types';\n\ntype RuleOptions<TAttributes extends WardAttributes> = Readonly<{\n when?: WardCondition<TAttributes>;\n}>;\n\nfunction buildRules<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] {\n const roles = Object.freeze(Array.isArray(role) ? [...role] : [role]);\n\n return actions.map((action) => ({\n action,\n ...(options?.when === undefined ? {} : { condition: options.when }),\n effect,\n resource,\n roles,\n }));\n}\n\nexport const allow = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('allow', role, resource, actions, options);\n\nexport const deny = <\n TAction extends string = string,\n TResource extends string = string,\n TAttributes extends WardAttributes = WardAttributes,\n>(\n role: string | readonly string[],\n resource: WardPattern<TResource>,\n actions: readonly WardPattern<TAction>[],\n options?: RuleOptions<TAttributes>,\n): WardRule<TAction, TResource, TAttributes>[] => buildRules('deny', role, resource, actions, options);\n\nconst owns =\n <TAttributes extends WardAttributes = WardAttributes>(\n attribute: keyof TAttributes & string,\n ): WardCondition<TAttributes> =>\n ({ attributes, principal }) =>\n principal !== null && attributes?.[attribute] === principal.id;\nconst and =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.every((condition) => condition(input));\nconst or =\n <TAttributes extends WardAttributes = WardAttributes>(\n ...conditions: readonly WardCondition<TAttributes>[]\n ): WardCondition<TAttributes> =>\n (input) =>\n conditions.some((condition) => condition(input));\nconst not =\n <TAttributes extends WardAttributes = WardAttributes>(\n condition: WardCondition<TAttributes>,\n ): WardCondition<TAttributes> =>\n (input) =>\n !condition(input);\n\nexport const predicate = { and, not, or, owns } as const;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vielzeug/ward",
|
|
3
|
-
"version": "26.10.
|
|
3
|
+
"version": "26.10.5",
|
|
4
4
|
"description": "Ordered authorization rules with immutable policies and typed decisions",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"test": "vitest"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@vielzeug/arsenal": "26.10.
|
|
43
|
+
"@vielzeug/arsenal": "26.10.5"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^26.6.2",
|