@vielzeug/ward 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -35
- package/dist/_compile.cjs +1 -1
- package/dist/_compile.cjs.map +1 -1
- package/dist/_compile.d.ts +5 -23
- package/dist/_compile.d.ts.map +1 -1
- package/dist/_compile.js +40 -35
- package/dist/_compile.js.map +1 -1
- package/dist/_match.cjs +1 -1
- package/dist/_match.cjs.map +1 -1
- package/dist/_match.d.ts +9 -19
- package/dist/_match.d.ts.map +1 -1
- package/dist/_match.js +40 -37
- package/dist/_match.js.map +1 -1
- package/dist/errors.cjs +1 -1
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +3 -16
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -3
- package/dist/errors.js.map +1 -1
- package/dist/factory.cjs +1 -1
- package/dist/factory.cjs.map +1 -1
- package/dist/factory.d.ts +14 -10
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +71 -119
- package/dist/factory.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/rules.cjs +2 -0
- package/dist/rules.cjs.map +1 -0
- package/dist/rules.d.ts +15 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +26 -0
- package/dist/rules.js.map +1 -0
- package/dist/types.d.ts +63 -132
- package/dist/types.d.ts.map +1 -1
- package/dist/ward.cjs +1 -1
- package/dist/ward.cjs.map +1 -1
- package/dist/ward.iife.js +1 -1
- package/dist/ward.iife.js.map +1 -1
- package/dist/ward.js +1 -1
- package/dist/ward.js.map +1 -1
- package/package.json +7 -7
- package/dist/_conflict.cjs +0 -2
- package/dist/_conflict.cjs.map +0 -1
- package/dist/_conflict.d.ts +0 -10
- package/dist/_conflict.d.ts.map +0 -1
- package/dist/_conflict.js +0 -51
- package/dist/_conflict.js.map +0 -1
- package/dist/_dev.cjs +0 -2
- package/dist/_dev.cjs.map +0 -1
- package/dist/_dev.d.ts +0 -8
- package/dist/_dev.d.ts.map +0 -1
- package/dist/_dev.js +0 -6
- package/dist/_dev.js.map +0 -1
- package/dist/builder.cjs +0 -2
- package/dist/builder.cjs.map +0 -1
- package/dist/builder.d.ts +0 -71
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js +0 -39
- package/dist/builder.js.map +0 -1
package/dist/ward.iife.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ward.iife.js","names":[],"sources":["../src/builder.ts","../src/constants.ts","../src/errors.ts","../src/_compile.ts","../src/resource.ts","../src/_match.ts","../src/_conflict.ts","../src/_dev.ts","../src/factory.ts"],"sourcesContent":["import type { WILDCARD } from './constants';\nimport type { WardPredicate, WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// RuleOptions — shared options for allow/deny\n// ---------------------------------------------------------------------------\n\ntype RuleOptions<TData> = {\n priority?: number;\n when?: WardPredicate<TData>;\n};\n\n// ---------------------------------------------------------------------------\n// buildRules — shared implementation\n// ---------------------------------------------------------------------------\n\nfunction buildRules<TAction extends string, TData>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options: RuleOptions<TData> | undefined,\n): WardRule<TAction, TData>[] {\n return actions.map((action) => ({\n action,\n effect,\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n resource,\n role,\n ...(options?.when !== undefined ? { when: options.when } : {}),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// allow / deny — ergonomic factories\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action with `effect: 'allow'`.\n *\n * Reads naturally: \"allow editor to read/update posts\".\n *\n * @example\n * ```ts\n * allow('editor', 'posts', ['read', 'update'])\n * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })\n * ```\n */\nexport function allow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return buildRules('allow', role, resource, actions, options);\n}\n\n/**\n * Creates one `WardRule` per action with `effect: 'deny'`.\n *\n * Reads naturally: \"deny blocked from reading posts\".\n *\n * @example\n * ```ts\n * deny('blocked', 'posts', ['read', 'update'])\n * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })\n * ```\n */\nexport function deny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return buildRules('deny', role, resource, actions, options);\n}\n\n// ---------------------------------------------------------------------------\n// predicate — grouped predicate helpers (R10)\n// ---------------------------------------------------------------------------\n\n/**\n * Grouped predicate factories. Import as a namespace to avoid name collisions:\n * ```ts\n * import { predicate } from '@vielzeug/ward';\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })\n * ```\n */\nexport const predicate = {\n /**\n * Returns a `WardPredicate` that combines all given predicates with AND semantics —\n * all must return `true` for the rule to match.\n */\n and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.every((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that inverts the given predicate.\n */\n not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData> {\n return (ctx) => !pred(ctx);\n },\n\n /**\n * Returns a `WardPredicate` that combines all given predicates with OR semantics —\n * at least one must return `true` for the rule to match.\n */\n or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.some((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`. Use to express ownership constraints.\n *\n * Must be used with a rule that requires authentication (non-`ANONYMOUS` role).\n * Predicates only execute for authenticated principals — pairing `owns` with an\n * `ANONYMOUS`-role rule produces a rule that can never match because the predicate\n * is skipped for unauthenticated requests.\n *\n * @example\n * ```ts\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * ```\n */\n owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n ): WardPredicate<TData> {\n return ({ data, principal }) => {\n if (!data || typeof data !== 'object') return false;\n\n const record = data as Record<string, unknown>;\n\n if (!Object.hasOwn(record, attributeKey)) return false;\n\n return record[attributeKey] === principal.id;\n };\n },\n} as const;\n","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 or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/**\n * Thrown when a `when` predicate in a ward rule throws an exception.\n *\n * Catch this to distinguish predicate failures from other errors:\n * ```ts\n * try {\n * ward.explain(principal, resource, action, data);\n * } catch (e) {\n * if (e instanceof WardPredicateError) {\n * console.error(`Predicate in Rule[${e.ruleIndex}] threw`, e.cause);\n * }\n * }\n * ```\n */\nexport class WardPredicateError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n\n super(`Rule[${ruleIndex}] threw: ${msg}`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n","import { WILDCARD } from './constants';\nimport { WardConfigError } from './errors';\nimport type { NormalizedWardRule, WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// Internal types (shared across modules)\n// ---------------------------------------------------------------------------\n\n/** A compiled entry stores the normalized rule plus pre-computed lookup values. */\nexport type CompiledEntry<TAction extends string, TData> = {\n /** Deny bonus (1 for deny, 0 for allow): tiebreaker when priority and score match. */\n denyBonus: 0 | 1;\n /** Original rule index, used in predicate error messages. */\n index: number;\n /** Resolved priority (defaults to 0 when not authored). */\n priority: number;\n /** Normalized roles array (always an array). */\n roles: readonly string[];\n /** The normalized, frozen compiled rule. */\n rule: Readonly<NormalizedWardRule<TAction, TData>>;\n /** Specificity score (0–5): roleScore(0|1) + resourceScore(0|1|2) + actionScore(0|1|2). */\n score: number;\n};\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport function validateRuleInput<TAction extends string, TData>(rule: WardRule<TAction, TData>, index: number): void {\n const at = `Rule[${index}]`;\n const roles = Array.isArray(rule.role) ? rule.role : [rule.role];\n\n if (roles.length === 0 || roles.some((r) => typeof r !== 'string' || !r.trim())) {\n throw new WardConfigError(`${at}.role must be a non-empty string or non-empty array of strings`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if ((rule.resource as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.resource '${String(rule.resource)}' ends with ':' — did you mean '${String(rule.resource)}*'?`,\n );\n }\n\n if (typeof rule.action !== 'string' || !(rule.action as string).trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if ((rule.action as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.action '${String(rule.action)}' ends with ':' — did you mean '${String(rule.action)}*'?`,\n );\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.priority !== undefined && (typeof rule.priority !== 'number' || !Number.isFinite(rule.priority))) {\n throw new WardConfigError(`${at}.priority must be a finite number`);\n }\n\n if (rule.when !== undefined && typeof rule.when !== 'function') {\n throw new WardConfigError(`${at}.when must be a function`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Specificity\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the specificity score for a single pattern field:\n * - 0: global wildcard (`*`)\n * - 1: namespace wildcard (e.g. `posts:*` or `read:*`)\n * - 2: exact string\n */\nexport function patternScore(pattern: string): number {\n if (pattern === WILDCARD) return 0;\n\n if (pattern.endsWith(':*')) return 1;\n\n return 2;\n}\n\n/**\n * Specificity: role(0|1) + resource(0|1|2) + action(0|1|2) = max 5.\n * Higher score = more specific = wins ties in priority.\n */\nfunction specificity<TAction extends string, TData>(rule: Readonly<NormalizedWardRule<TAction, TData>>): number {\n const roleScore = rule.role.includes(WILDCARD) ? 0 : 1;\n\n return roleScore + patternScore(rule.resource as string) + patternScore(rule.action as string);\n}\n\n// ---------------------------------------------------------------------------\n// Compilation\n// ---------------------------------------------------------------------------\n\nexport function compileEntry<TAction extends string, TData>(\n input: WardRule<TAction, TData>,\n index: number,\n): CompiledEntry<TAction, TData> {\n validateRuleInput(input, index);\n\n const rawRoles = Array.isArray(input.role) ? [...input.role] : [input.role];\n const roles: readonly string[] = Object.freeze([...new Set(rawRoles)]);\n\n const rule = Object.freeze({\n action: input.action,\n effect: input.effect,\n priority: input.priority ?? 0,\n resource: input.resource,\n role: roles,\n ...(input.when !== undefined ? { when: input.when } : {}),\n }) as Readonly<NormalizedWardRule<TAction, TData>>;\n\n const score = specificity(rule);\n const priority = rule.priority;\n const denyBonus: 0 | 1 = rule.effect === 'deny' ? 1 : 0;\n\n return { denyBonus, index, priority, roles, rule, score };\n}\n","import { WILDCARD } from './constants';\n\n/**\n * Matches a rule pattern against a concrete value string.\n * Works for both resource patterns and action patterns (action hierarchy).\n *\n * Matching rules (evaluated in order):\n * 1. `WILDCARD` (`*`) matches any value string.\n * 2. An exact string matches itself.\n * 3. A colon-namespaced pattern ending in `:*` (e.g. `posts:*` or `read:*`)\n * matches any string that starts with the given namespace prefix followed by `:`.\n * Example: `posts:*` matches `posts:123`, `posts:draft:42`, etc.\n * `read:*` matches `read:own`, `read:all`, etc.\n *\n * @example\n * matchesPattern('*', 'posts:123') // true — global wildcard\n * matchesPattern('posts', 'posts') // true — exact\n * matchesPattern('posts:*', 'posts:123') // true — namespace wildcard\n * matchesPattern('posts:*', 'comments:1') // false — different namespace\n * matchesPattern('posts:42', 'posts:42') // true — exact namespaced id\n * matchesPattern('read:*', 'read:own') // true — action namespace wildcard\n * matchesPattern('read:*', 'write:all') // false — different action namespace\n */\nexport function matchesPattern(pattern: string, value: string): boolean {\n if (pattern === WILDCARD) return true;\n\n if (pattern === value) return true;\n\n // Namespace wildcard: \"namespace:*\" matches \"namespace:anything\"\n if (pattern.endsWith(':*')) {\n const ns = pattern.slice(0, -1); // keep the trailing colon as prefix delimiter\n\n return value.startsWith(ns);\n }\n\n return false;\n}\n\n/**\n * Returns true if every concrete value that matches `narrow` also matches `broad`.\n * This is the coverage relation used for rule conflict detection.\n *\n * Pattern hierarchy (most to least permissive): `*` > `ns:*` > exact\n *\n * Unlike `matchesPattern(broad, narrow)` — which works accidentally — this\n * function explicitly documents and tests the coverage semantics so it remains\n * correct if new pattern syntax is added in the future.\n *\n * @example\n * patternCovers('*', 'posts:*') // true — * covers everything\n * patternCovers('posts:*', 'posts:123') // true — namespace covers exact\n * patternCovers('posts:*', 'posts:*') // true — same pattern covers itself\n * patternCovers('posts:*', '*') // false — namespace does not cover global wildcard\n * patternCovers('posts', 'posts:*') // false — exact does not cover namespace\n */\nexport function patternCovers(broad: string, narrow: string): boolean {\n // Global wildcard covers everything\n if (broad === WILDCARD) return true;\n\n // Nothing else covers global wildcard\n if (narrow === WILDCARD) return false;\n\n // Namespace wildcard: covers any value starting with the same namespace prefix,\n // including nested namespace wildcards (\"posts:*\" covers \"posts:sub:*\") and exact IDs.\n if (broad.endsWith(':*')) {\n return narrow.startsWith(broad.slice(0, -1));\n }\n\n // Exact only covers itself\n return broad === narrow;\n}\n","import type { CompiledEntry } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\nimport type { Principal, UserPrincipal, WardDecision } from './types';\n\n// ---------------------------------------------------------------------------\n// Principal validation\n// ---------------------------------------------------------------------------\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const p = input as Record<string, unknown>;\n\n if (typeof p.id !== 'string' || !p.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(p.roles) || p.roles.some((r) => typeof r !== 'string' || !(r as string).trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function validatePrincipal(principal: Principal): void {\n if (principal !== null) {\n assertUserPrincipal(principal);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Role matching\n// ---------------------------------------------------------------------------\n\n/** Returns true if the principal's role set intersects with the rule's roles. */\nexport function principalMatchesRoles(roles: readonly string[], principal: Principal): boolean {\n if (principal === null) {\n return roles.includes(ANONYMOUS);\n }\n\n if (roles.every((r) => r === ANONYMOUS)) return false;\n\n if (roles.includes(WILDCARD)) return true;\n\n return roles.some((role) => principal.roles.includes(role));\n}\n\n// ---------------------------------------------------------------------------\n// Rule matching\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a rule applies to a given request.\n *\n * @param action - Pass `undefined` to skip the action check (used by `rulesInScope`).\n * @param data - Data payload; passed to `when` predicates.\n * @param skipPredicate - When true, omit the `when` evaluation even when data is present.\n */\nexport function matchesRule<TAction extends string, TData>(\n entry: CompiledEntry<TAction, TData>,\n principal: Principal,\n resource: string,\n action: TAction | undefined,\n data: TData | undefined,\n skipPredicate = false,\n): boolean {\n if (!principalMatchesRoles(entry.roles, principal)) return false;\n\n if (!matchesPattern(entry.rule.resource as string, resource)) return false;\n\n if (action !== undefined && !matchesPattern(entry.rule.action as string, action)) return false;\n\n if (skipPredicate || !entry.rule.when) return true;\n\n if (principal === null) return false;\n\n try {\n const result: unknown = entry.rule.when({ data, principal });\n\n if (result instanceof Promise) {\n throw new TypeError(\n `Rule[${entry.index}] when() returned a Promise. Async predicates are not supported — use a synchronous predicate.`,\n );\n }\n\n return result as boolean;\n } catch (err) {\n throw new WardPredicateError(entry.index, err);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Winner selection\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if `challenger` would displace `current` as the pickWinner result.\n */\nexport function isOverriddenBy<TAction extends string, TData>(\n current: CompiledEntry<TAction, TData>,\n challenger: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n challenger.priority > current.priority ||\n (challenger.priority === current.priority && challenger.score > current.score) ||\n (challenger.priority === current.priority &&\n challenger.score === current.score &&\n challenger.denyBonus > current.denyBonus)\n );\n}\n\nexport function pickWinner<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n): CompiledEntry<TAction, TData> | undefined {\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, action, data)) continue;\n\n if (!winner || isOverriddenBy(winner, entry)) {\n winner = entry;\n }\n }\n\n return winner;\n}\n\nexport function toDecision<TAction extends string, TData>(\n winner: CompiledEntry<TAction, TData> | undefined,\n): WardDecision<TAction, TData> {\n if (!winner) return { allowed: false, reason: 'no-matching-rule' };\n\n if (winner.rule.effect === 'deny') {\n return {\n allowed: false,\n reason: 'explicit-deny',\n rule: winner.rule,\n };\n }\n\n return { allowed: true, rule: winner.rule };\n}\n","import type { CompiledEntry } from './_compile';\nimport { isOverriddenBy } from './_match';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { patternCovers } from './resource';\nimport type { WardConflict } from './types';\n\n// ---------------------------------------------------------------------------\n// Role coverage helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if every principal that satisfies `narrowRoles` also satisfies `broadRoles`.\n */\nfunction roleCoversRoles(broadRoles: readonly string[], narrowRoles: readonly string[]): boolean {\n const narrowHasAnonymous = narrowRoles.includes(ANONYMOUS);\n const narrowHasAuthenticated = narrowRoles.some((r) => r !== ANONYMOUS);\n\n if (narrowHasAnonymous && !broadRoles.includes(ANONYMOUS)) return false;\n\n if (narrowHasAuthenticated) {\n if (broadRoles.includes(WILDCARD)) return true;\n\n return narrowRoles.filter((r) => r !== ANONYMOUS).every((r) => broadRoles.includes(r));\n }\n\n return true;\n}\n\nfunction rolesSetsEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n\n const setA = new Set(a);\n\n return b.every((r) => setA.has(r));\n}\n\n/** Returns true if every request that triggers `narrow` would also trigger `broad`. */\nfunction ruleCovers<TAction extends string, TData>(\n broad: CompiledEntry<TAction, TData>,\n narrow: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n roleCoversRoles(broad.roles, narrow.roles) &&\n patternCovers(broad.rule.resource as string, narrow.rule.resource as string) &&\n patternCovers(broad.rule.action as string, narrow.rule.action as string)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Conflict detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detects conflicts between compiled entries.\n *\n * @complexity O(n²) in the number of rules. Scales to hundreds of rules\n * with negligible overhead; for very large auto-generated policies, use `maxConflicts`.\n */\nexport function computeConflicts<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n maxConflicts: number,\n): WardConflict<TAction, TData>[] {\n const conflicts: WardConflict<TAction, TData>[] = [];\n\n for (let i = 0; i < entries.length && conflicts.length < maxConflicts; i++) {\n for (let j = i + 1; j < entries.length && conflicts.length < maxConflicts; j++) {\n const a = entries[i];\n const b = entries[j];\n\n if (\n !a.rule.when &&\n !b.rule.when &&\n rolesSetsEqual(a.roles, b.roles) &&\n a.rule.resource === b.rule.resource &&\n a.rule.action === b.rule.action\n ) {\n conflicts.push({\n indexA: a.index,\n indexB: b.index,\n kind: 'duplicate',\n ruleA: a.rule,\n ruleB: b.rule,\n });\n continue;\n }\n\n const bCanOverrideA = isOverriddenBy(a, b);\n\n if (!bCanOverrideA && !a.rule.when && ruleCovers(a, b)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: b.index,\n shadowedRule: b.rule,\n shadowingIndex: a.index,\n shadowingRule: a.rule,\n });\n } else if (bCanOverrideA && !b.rule.when && ruleCovers(b, a)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: a.index,\n shadowedRule: a.rule,\n shadowingIndex: b.index,\n shadowingRule: b.rule,\n });\n }\n }\n }\n\n return conflicts;\n}\n","import type { CompiledEntry } from './_compile';\n\nimport { ANONYMOUS } from './constants';\n\nconst isDev = !(globalThis as { __WARD_PROD__?: boolean }).__WARD_PROD__;\n\n/** @internal @security Messages may include user-supplied role names. */\nfunction warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/ward] ${msg}`);\n}\n\n/**\n * Emit a development-only warning when an ANONYMOUS-role rule has a `when` predicate.\n * The predicate is skipped for unauthenticated principals, so the rule can never match\n * anonymous requests — likely a policy authoring mistake.\n */\nexport function warnAnonymousPredicates<TAction extends string, TData>(\n entries: readonly CompiledEntry<TAction, TData>[],\n): void {\n if (!isDev) return;\n\n for (const entry of entries) {\n if (!entry.rule.when) continue;\n\n const hasAnonymous = entry.roles.includes(ANONYMOUS);\n const hasOnlyAnonymous = hasAnonymous && entry.roles.length === 1;\n\n if (hasOnlyAnonymous) {\n warn(\n `Rule[${entry.index}] pairs role '${ANONYMOUS}' with a \\`when\\` predicate. ` +\n 'Predicates are skipped for unauthenticated principals, so this rule can never match anonymous requests.',\n );\n }\n }\n}\n","import type { CompiledEntry } from './_compile';\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { warnAnonymousPredicates } from './_dev';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\nimport type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n NormalizedWardRule,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nconst warn = warnAnonymousPredicates;\n\n// ---------------------------------------------------------------------------\n// Shared loop cores (validation-free; used by both public API and forUser)\n// ---------------------------------------------------------------------------\n\nfunction coreAllowedActions<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data: TData | undefined,\n): TAction[] {\n const seen = new Set<TAction>();\n const result: TAction[] = [];\n\n for (const action of knownActions) {\n if (seen.has(action)) continue;\n\n seen.add(action);\n\n const winner = pickWinner(entries, principal, resource, action, data);\n\n if (winner?.rule.effect === 'allow') result.push(action);\n }\n\n return result;\n}\n\nfunction coreRulesInScope<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n data: TData | undefined,\n): NormalizedWardRule<TAction, TData>[] {\n const skipPredicate = data === undefined;\n const result: NormalizedWardRule<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule as NormalizedWardRule<TAction, TData>);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly (WardRule<TAction, TData> | readonly WardRule<TAction, TData>[])[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n if (options.maxConflicts !== undefined) {\n if (!Number.isFinite(options.maxConflicts) || options.maxConflicts < 0) {\n throw new WardConfigError('maxConflicts must be a finite non-negative number.');\n }\n }\n\n if (options.onConflict !== undefined && typeof options.onConflict !== 'function') {\n throw new WardConfigError('onConflict must be a function.');\n }\n\n const { maxConflicts = Infinity } = options;\n const tappers = new Set<(event: WardEvent<TAction, TData>) => void>();\n const flat: WardRule<TAction, TData>[] = [];\n\n for (const entry of rules) {\n if (Array.isArray(entry)) {\n for (const rule of entry) flat.push(rule);\n } else {\n flat.push(entry as WardRule<TAction, TData>);\n }\n }\n\n const entries = flat.map((rule, i) => compileEntry(rule, i));\n\n // Warn in development when an ANONYMOUS-role rule has a predicate.\n warn(entries);\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function emitTap(event: WardEvent<TAction, TData>): void {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must not affect ward behavior.\n }\n }\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n emitTap({ action, data, decision, principal, resource });\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n // Request objects avoid call-site ambiguity between resource/action/data and\n // make later API growth additive instead of positional-breaking.\n\n function explain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[] {\n const { data, knownActions, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(\n input: WardRulesInScopeInput<TData>,\n ): ReadonlyArray<Readonly<NormalizedWardRule<TAction, TData>>> {\n const { data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (input: BoundWardAllowedActionsInput<TAction, TData>) =>\n coreAllowedActions(entries, snap, input.resource, input.knownActions, input.data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (input: BoundWardDecisionInput<TAction, TData>) =>\n evaluateAndLog(snap, input.resource, input.action, input.data),\n rulesInScope: (input: BoundWardRulesInScopeInput<TData>) =>\n coreRulesInScope(entries, snap, input.resource, input.data),\n trace: (input: BoundWardDecisionInput<TAction, TData>) =>\n trace({ action: input.action, data: input.data, principal: snap, resource: input.resource }),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n function tap(handler: (event: WardEvent<TAction, TData>) => void, opts?: { signal?: AbortSignal }): () => void {\n tappers.add(handler);\n\n const onAbort = () => tappers.delete(handler);\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n tappers.delete(handler);\n return () => {};\n }\n opts.signal.addEventListener('abort', onAbort, { once: true });\n }\n\n return () => {\n tappers.delete(handler);\n opts?.signal?.removeEventListener('abort', onAbort);\n };\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, tap, trace };\n}\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 return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n","import { WILDCARD } from './constants';\n\n/**\n * Matches a rule pattern against a concrete value string.\n * Works for both resource patterns and action patterns (action hierarchy).\n *\n * Matching rules (evaluated in order):\n * 1. `WILDCARD` (`*`) matches any value string.\n * 2. An exact string matches itself.\n * 3. A colon-namespaced pattern ending in `:*` (e.g. `posts:*` or `read:*`)\n * matches any string that starts with the given namespace prefix followed by `:`.\n * Example: `posts:*` matches `posts:123`, `posts:draft:42`, etc.\n * `read:*` matches `read:own`, `read:all`, etc.\n *\n * @example\n * matchesPattern('*', 'posts:123') // true — global wildcard\n * matchesPattern('posts', 'posts') // true — exact\n * matchesPattern('posts:*', 'posts:123') // true — namespace wildcard\n * matchesPattern('posts:*', 'comments:1') // false — different namespace\n * matchesPattern('posts:42', 'posts:42') // true — exact namespaced id\n * matchesPattern('read:*', 'read:own') // true — action namespace wildcard\n * matchesPattern('read:*', 'write:all') // false — different action namespace\n */\nexport function matchesPattern(pattern: string, value: string): boolean {\n if (pattern === WILDCARD) return true;\n\n if (pattern === value) return true;\n\n // Namespace wildcard: \"namespace:*\" matches \"namespace:anything\"\n if (pattern.endsWith(':*')) {\n const ns = pattern.slice(0, -1); // keep the trailing colon as prefix delimiter\n\n return value.startsWith(ns);\n }\n\n return false;\n}\n\n/**\n * Returns true if every concrete value that matches `narrow` also matches `broad`.\n * This is the coverage relation used for rule conflict detection.\n *\n * Pattern hierarchy (most to least permissive): `*` > `ns:*` > exact\n *\n * Unlike `matchesPattern(broad, narrow)` — which works accidentally — this\n * function explicitly documents and tests the coverage semantics so it remains\n * correct if new pattern syntax is added in the future.\n *\n * @example\n * patternCovers('*', 'posts:*') // true — * covers everything\n * patternCovers('posts:*', 'posts:123') // true — namespace covers exact\n * patternCovers('posts:*', 'posts:*') // true — same pattern covers itself\n * patternCovers('posts:*', '*') // false — namespace does not cover global wildcard\n * patternCovers('posts', 'posts:*') // false — exact does not cover namespace\n */\nexport function patternCovers(broad: string, narrow: string): boolean {\n // Global wildcard covers everything\n if (broad === WILDCARD) return true;\n\n // Nothing else covers global wildcard\n if (narrow === WILDCARD) return false;\n\n // Namespace wildcard: covers any value starting with the same namespace prefix,\n // including nested namespace wildcards (\"posts:*\" covers \"posts:sub:*\") and exact IDs.\n if (broad.endsWith(':*')) {\n return narrow.startsWith(broad.slice(0, -1));\n }\n\n // Exact only covers itself\n return broad === narrow;\n}\n","import { snapshotAttributes } from './_compile';\nimport { WardConditionError, WardConfigError } from './errors';\nimport { matchesPattern } from './resource';\nimport type {\n Principal,\n UserPrincipal,\n WardAttributes,\n WardAttributeValue,\n WardDecisionInput,\n WardRule,\n} from './types';\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const principal = input as Record<string, unknown>;\n\n if (typeof principal.id !== 'string' || !principal.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(principal.roles) || principal.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function snapshotPrincipal(principal: Principal | undefined): Principal {\n if (principal === undefined || principal === null) return null;\n\n assertUserPrincipal(principal);\n\n return Object.freeze({\n ...(principal.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(principal.attributes, 'Principal.attributes') }),\n id: principal.id,\n roles: Object.freeze([...principal.roles]),\n });\n}\n\nexport function snapshotDecisionInput<\n TAction extends string,\n TResource extends string,\n TAttributes extends WardAttributes,\n>(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n): WardDecisionInput<TAction, TResource, TAttributes> & {\n principal: Principal;\n} {\n if (typeof input.action !== 'string' || !input.action.trim()) {\n throw new WardConfigError('Decision.action must be a non-empty string');\n }\n if (typeof input.resource !== 'string' || !input.resource.trim()) {\n throw new WardConfigError('Decision.resource must be a non-empty string');\n }\n\n return Object.freeze({\n action: input.action,\n ...(input.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(input.attributes, 'Decision.attributes') as TAttributes }),\n principal: snapshotPrincipal(input.principal),\n resource: input.resource,\n });\n}\n\nfunction deepEqual(left: WardAttributeValue, right: WardAttributeValue): boolean {\n if (Object.is(left, right)) return true;\n if (typeof left !== 'object' || typeof right !== 'object' || left === null || right === null) return false;\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((value, index) => deepEqual(value, right[index]))\n );\n }\n\n const leftRecord = left as Record<string, WardAttributeValue>;\n const rightRecord = right as Record<string, WardAttributeValue>;\n const leftKeys = Object.keys(leftRecord);\n const rightKeys = Object.keys(rightRecord);\n\n return (\n leftKeys.length === rightKeys.length &&\n leftKeys.every((key) => Object.hasOwn(rightRecord, key) && deepEqual(leftRecord[key], rightRecord[key]))\n );\n}\n\nexport function attributesMatch(expected: WardAttributes | undefined, actual: WardAttributes | undefined): boolean {\n if (!expected) return true;\n if (!actual) return false;\n\n return Object.keys(expected).every((key) => Object.hasOwn(actual, key) && deepEqual(expected[key], actual[key]));\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n (typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function') ||\n (typeof value === 'function' && 'then' in value && typeof value.then === 'function')\n );\n}\n\nexport function ruleMatches<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n): boolean {\n if (!matchesPattern(rule.action, input.action)) return false;\n if (!matchesPattern(rule.resource, input.resource)) return false;\n if (!attributesMatch(rule.attributes, input.attributes)) return false;\n if (!rule.condition) return true;\n\n try {\n const result: unknown = rule.condition({ attributes: input.attributes, principal: input.principal });\n\n if (isThenable(result)) {\n throw new TypeError('Async conditions are not supported; return a boolean');\n }\n if (typeof result !== 'boolean') {\n throw new TypeError('Conditions must return a boolean');\n }\n\n return result;\n } catch (error) {\n throw new WardConditionError(index, error);\n }\n}\n","import { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport { WardConfigError } from './errors';\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. Every key in its declarative `attributes` is present and equal in the\n * request's `attributes` (deep equality).\n * 4. 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 tappers = new Set<(event: WardEvent<TAction, TResource, TAttributes>) => void>();\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 if (tappers.size === 0) return;\n\n const event = Object.freeze({ decision, input, type: 'decision' as const });\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {}\n }\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 knownActions: readonly TAction[];\n principal?: Principal;\n resource: TResource;\n }): TAction[] {\n const seen = new Set<TAction>();\n\n return input.knownActions.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 function tap(\n handler: (event: WardEvent<TAction, TResource, TAttributes>) => void,\n options?: { readonly signal?: AbortSignal },\n ): () => void {\n if (typeof handler !== 'function') throw new WardConfigError('tap handler must be a function');\n if (options?.signal?.aborted) return () => {};\n\n tappers.add(handler);\n const onAbort = () => tappers.delete(handler);\n options?.signal?.addEventListener('abort', onAbort, { once: true });\n\n return () => {\n tappers.delete(handler);\n options?.signal?.removeEventListener('abort', onAbort);\n };\n }\n\n return { allowedActions, checkAll, decide, forPrincipal, rules: compiled, tap };\n}\n","import { ANONYMOUS, WILDCARD } from './constants';\nimport type { WardAttributes, WardCondition, WardPattern, WardRule } from './types';\n\ntype RuleOptions<TAttributes extends WardAttributes> = Readonly<{\n when?: WardCondition<TAttributes>;\n}>;\n\nfunction roleCondition<TAttributes extends WardAttributes>(\n role: string | readonly string[],\n): WardCondition<TAttributes> {\n const roles = Object.freeze(Array.isArray(role) ? [...role] : [role]);\n\n return ({ principal }) =>\n roles.some((candidate) =>\n candidate === ANONYMOUS\n ? principal === null\n : candidate === WILDCARD\n ? principal !== null\n : principal?.roles.includes(candidate),\n );\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 matchesRole = roleCondition<TAttributes>(role);\n const condition = options?.when\n ? (input: Parameters<WardCondition<TAttributes>>[0]) => matchesRole(input) && options.when!(input)\n : matchesRole;\n\n return actions.map((action) => ({ action, condition, effect, resource }));\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 hasRole =\n <TAttributes extends WardAttributes = WardAttributes>(role: string): WardCondition<TAttributes> =>\n ({ principal }) =>\n principal?.roles.includes(role) ?? false;\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, hasRole, not, or, owns } as const;\n"],"mappings":"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"}
|
package/dist/ward.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
var e=`*`,t=`anonymous`,n=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},r=class extends n{},i=class extends n{ruleIndex;constructor(e,t){super(`Rule[${e}].condition failed`,{cause:t}),this.ruleIndex=e}};function a(e,t,n){if(e===null||typeof e==`boolean`||typeof e==`string`)return e;if(typeof e==`number`){if(!Number.isFinite(e))throw new r(`${t} must contain only finite numbers`);return e}if(typeof e!=`object`)throw new r(`${t} must contain only JSON-compatible values`);if(n.has(e))throw new r(`${t} must not contain circular references`);let i=Object.getPrototypeOf(e);if(!Array.isArray(e)&&i!==Object.prototype&&i!==null)throw new r(`${t} must contain only arrays and plain objects`);n.add(e);try{return Object.freeze(Array.isArray(e)?Array.from(e,(e,r)=>a(e,`${t}[${r}]`,n)):Object.fromEntries(Object.keys(e).map(r=>[r,a(e[r],`${t}.${r}`,n)])))}finally{n.delete(e)}}function o(e,t){if(e!==void 0){if(typeof e!=`object`||!e||Array.isArray(e))throw new r(`${t} must be a plain object`);return a(e,t,new Set)}}function s(e,t){let n=`Rule[${t}]`;if(typeof e!=`object`||!e||Array.isArray(e))throw new r(`${n} must be a plain object`);let i=Object.getPrototypeOf(e);if(i!==Object.prototype&&i!==null)throw new r(`${n} must be a plain object`);if(typeof e.action!=`string`||!e.action.trim())throw new r(`${n}.action must be a non-empty string`);if(e.action.endsWith(`:`))throw new r(`${n}.action '${e.action}' ends with ':' — did you mean '${e.action}*'?`);if(typeof e.resource!=`string`||!e.resource.trim())throw new r(`${n}.resource must be a non-empty string`);if(e.resource.endsWith(`:`))throw new r(`${n}.resource '${e.resource}' ends with ':' — did you mean '${e.resource}*'?`);if(e.effect!==`allow`&&e.effect!==`deny`)throw new r(`${n}.effect must be "allow" or "deny"`);if(e.condition!==void 0&&typeof e.condition!=`function`)throw new r(`${n}.condition must be a function`);return Object.freeze({action:e.action,...e.attributes===void 0?{}:{attributes:o(e.attributes,`${n}.attributes`)},...e.condition===void 0?{}:{condition:e.condition},effect:e.effect,resource:e.resource})}function c(e,t){if(e===`*`||e===t)return!0;if(e.endsWith(`:*`)){let n=e.slice(0,-1);return t.startsWith(n)}return!1}function l(e,t){return e===`*`?!0:t===`*`?!1:e.endsWith(`:*`)?t.startsWith(e.slice(0,-1)):e===t}function u(e){if(typeof e!=`object`||!e)throw new r(`Invalid principal: expected { id: string, roles: string[] }`);let t=e;if(typeof t.id!=`string`||!t.id.trim())throw new r(`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 r(`Invalid principal: roles must be an array of non-empty strings`)}function d(e){return e==null?null:(u(e),Object.freeze({...e.attributes===void 0?{}:{attributes:o(e.attributes,`Principal.attributes`)},id:e.id,roles:Object.freeze([...e.roles])}))}function f(e){if(typeof e.action!=`string`||!e.action.trim())throw new r(`Decision.action must be a non-empty string`);if(typeof e.resource!=`string`||!e.resource.trim())throw new r(`Decision.resource must be a non-empty string`);return Object.freeze({action:e.action,...e.attributes===void 0?{}:{attributes:o(e.attributes,`Decision.attributes`)},principal:d(e.principal),resource:e.resource})}function p(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)=>p(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)&&p(n[e],r[e]))}function m(e,t){return e?t?Object.keys(e).every(n=>Object.hasOwn(t,n)&&p(e[n],t[n])):!1:!0}function h(e){return typeof e==`object`&&!!e&&`then`in e&&typeof e.then==`function`||typeof e==`function`&&`then`in e&&typeof e.then==`function`}function g(e,t,n){if(!c(e.action,n.action)||!c(e.resource,n.resource)||!m(e.attributes,n.attributes))return!1;if(!e.condition)return!0;try{let t=e.condition({attributes:n.attributes,principal:n.principal});if(h(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 i(t,e)}}function _(e=[]){let t=Object.freeze(e.flat().map((e,t)=>s(e,t))),n=new Set;function i(e,n){let r=f(e);for(let e=0;e<t.length;e++){let i=t[e];if(g(i,e,r)){let t=Object.freeze({effect:i.effect,matched:!0,reason:`rule[${e}] ${i.effect===`allow`?`allows`:`denies`} '${r.action}' on '${r.resource}'`,rule:i});return n&&a(r,t),t}}let i=Object.freeze({effect:`deny`,matched:!1,reason:`no matching rule for '${r.action}' on '${r.resource}' (default deny)`});return n&&a(r,i),i}function a(e,t){if(n.size===0)return;let r=Object.freeze({decision:t,input:e,type:`decision`});for(let e of n)try{e(r)}catch{}}function o(e){return i(e,!0)}function c(e){return e.map(o)}function l(e){let t=new Set;return e.knownActions.filter(n=>!t.has(n)&&(t.add(n),i({...e,action:n},!1).effect===`allow`))}function u(e){let t=d(e);return{allowedActions:e=>l({...e,principal:t}),checkAll:e=>c(e.map(e=>({...e,principal:t}))),decide:e=>o({...e,principal:t})}}function p(e,t){if(typeof e!=`function`)throw new r(`tap handler must be a function`);if(t?.signal?.aborted)return()=>{};n.add(e);let i=()=>n.delete(e);return t?.signal?.addEventListener(`abort`,i,{once:!0}),()=>{n.delete(e),t?.signal?.removeEventListener(`abort`,i)}}return{allowedActions:l,checkAll:c,decide:o,forPrincipal:u,rules:t,tap:p}}function v(e){let t=Object.freeze(Array.isArray(e)?[...e]:[e]);return({principal:e})=>t.some(t=>t===`anonymous`?e===null:t===`*`?e!==null:e?.roles.includes(t))}function y(e,t,n,r,i){let a=v(t),o=i?.when?e=>a(e)&&i.when(e):a;return r.map(t=>({action:t,condition:o,effect:e,resource:n}))}var b=(e,t,n,r)=>y(`allow`,e,t,n,r),x=(e,t,n,r)=>y(`deny`,e,t,n,r),S={and:(...e)=>t=>e.every(e=>e(t)),hasRole:e=>({principal:t})=>t?.roles.includes(e)??!1,not:e=>t=>!e(t),or:(...e)=>t=>e.some(e=>e(t)),owns:e=>({attributes:t,principal:n})=>n!==null&&t?.[e]===n.id};export{t as ANONYMOUS,e as WILDCARD,i as WardConditionError,r as WardConfigError,n as WardError,b as allow,_ as createWard,x as deny,c as matchesPattern,l as patternCovers,S 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/builder.ts","../src/constants.ts","../src/errors.ts","../src/_compile.ts","../src/resource.ts","../src/_match.ts","../src/_conflict.ts","../src/_dev.ts","../src/factory.ts"],"sourcesContent":["import type { WILDCARD } from './constants';\nimport type { WardPredicate, WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// RuleOptions — shared options for allow/deny\n// ---------------------------------------------------------------------------\n\ntype RuleOptions<TData> = {\n priority?: number;\n when?: WardPredicate<TData>;\n};\n\n// ---------------------------------------------------------------------------\n// buildRules — shared implementation\n// ---------------------------------------------------------------------------\n\nfunction buildRules<TAction extends string, TData>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options: RuleOptions<TData> | undefined,\n): WardRule<TAction, TData>[] {\n return actions.map((action) => ({\n action,\n effect,\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n resource,\n role,\n ...(options?.when !== undefined ? { when: options.when } : {}),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// allow / deny — ergonomic factories\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action with `effect: 'allow'`.\n *\n * Reads naturally: \"allow editor to read/update posts\".\n *\n * @example\n * ```ts\n * allow('editor', 'posts', ['read', 'update'])\n * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })\n * ```\n */\nexport function allow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return buildRules('allow', role, resource, actions, options);\n}\n\n/**\n * Creates one `WardRule` per action with `effect: 'deny'`.\n *\n * Reads naturally: \"deny blocked from reading posts\".\n *\n * @example\n * ```ts\n * deny('blocked', 'posts', ['read', 'update'])\n * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })\n * ```\n */\nexport function deny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return buildRules('deny', role, resource, actions, options);\n}\n\n// ---------------------------------------------------------------------------\n// predicate — grouped predicate helpers (R10)\n// ---------------------------------------------------------------------------\n\n/**\n * Grouped predicate factories. Import as a namespace to avoid name collisions:\n * ```ts\n * import { predicate } from '@vielzeug/ward';\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })\n * ```\n */\nexport const predicate = {\n /**\n * Returns a `WardPredicate` that combines all given predicates with AND semantics —\n * all must return `true` for the rule to match.\n */\n and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.every((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that inverts the given predicate.\n */\n not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData> {\n return (ctx) => !pred(ctx);\n },\n\n /**\n * Returns a `WardPredicate` that combines all given predicates with OR semantics —\n * at least one must return `true` for the rule to match.\n */\n or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.some((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`. Use to express ownership constraints.\n *\n * Must be used with a rule that requires authentication (non-`ANONYMOUS` role).\n * Predicates only execute for authenticated principals — pairing `owns` with an\n * `ANONYMOUS`-role rule produces a rule that can never match because the predicate\n * is skipped for unauthenticated requests.\n *\n * @example\n * ```ts\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * ```\n */\n owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n ): WardPredicate<TData> {\n return ({ data, principal }) => {\n if (!data || typeof data !== 'object') return false;\n\n const record = data as Record<string, unknown>;\n\n if (!Object.hasOwn(record, attributeKey)) return false;\n\n return record[attributeKey] === principal.id;\n };\n },\n} as const;\n","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 or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/**\n * Thrown when a `when` predicate in a ward rule throws an exception.\n *\n * Catch this to distinguish predicate failures from other errors:\n * ```ts\n * try {\n * ward.explain(principal, resource, action, data);\n * } catch (e) {\n * if (e instanceof WardPredicateError) {\n * console.error(`Predicate in Rule[${e.ruleIndex}] threw`, e.cause);\n * }\n * }\n * ```\n */\nexport class WardPredicateError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n\n super(`Rule[${ruleIndex}] threw: ${msg}`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n","import { WILDCARD } from './constants';\nimport { WardConfigError } from './errors';\nimport type { NormalizedWardRule, WardRule } from './types';\n\n// ---------------------------------------------------------------------------\n// Internal types (shared across modules)\n// ---------------------------------------------------------------------------\n\n/** A compiled entry stores the normalized rule plus pre-computed lookup values. */\nexport type CompiledEntry<TAction extends string, TData> = {\n /** Deny bonus (1 for deny, 0 for allow): tiebreaker when priority and score match. */\n denyBonus: 0 | 1;\n /** Original rule index, used in predicate error messages. */\n index: number;\n /** Resolved priority (defaults to 0 when not authored). */\n priority: number;\n /** Normalized roles array (always an array). */\n roles: readonly string[];\n /** The normalized, frozen compiled rule. */\n rule: Readonly<NormalizedWardRule<TAction, TData>>;\n /** Specificity score (0–5): roleScore(0|1) + resourceScore(0|1|2) + actionScore(0|1|2). */\n score: number;\n};\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nexport function validateRuleInput<TAction extends string, TData>(rule: WardRule<TAction, TData>, index: number): void {\n const at = `Rule[${index}]`;\n const roles = Array.isArray(rule.role) ? rule.role : [rule.role];\n\n if (roles.length === 0 || roles.some((r) => typeof r !== 'string' || !r.trim())) {\n throw new WardConfigError(`${at}.role must be a non-empty string or non-empty array of strings`);\n }\n\n if (typeof rule.resource !== 'string' || !rule.resource.trim()) {\n throw new WardConfigError(`${at}.resource must be a non-empty string`);\n }\n\n if ((rule.resource as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.resource '${String(rule.resource)}' ends with ':' — did you mean '${String(rule.resource)}*'?`,\n );\n }\n\n if (typeof rule.action !== 'string' || !(rule.action as string).trim()) {\n throw new WardConfigError(`${at}.action must be a non-empty string`);\n }\n\n if ((rule.action as string).endsWith(':')) {\n throw new WardConfigError(\n `${at}.action '${String(rule.action)}' ends with ':' — did you mean '${String(rule.action)}*'?`,\n );\n }\n\n if (rule.effect !== 'allow' && rule.effect !== 'deny') {\n throw new WardConfigError(`${at}.effect must be \"allow\" or \"deny\"`);\n }\n\n if (rule.priority !== undefined && (typeof rule.priority !== 'number' || !Number.isFinite(rule.priority))) {\n throw new WardConfigError(`${at}.priority must be a finite number`);\n }\n\n if (rule.when !== undefined && typeof rule.when !== 'function') {\n throw new WardConfigError(`${at}.when must be a function`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Specificity\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the specificity score for a single pattern field:\n * - 0: global wildcard (`*`)\n * - 1: namespace wildcard (e.g. `posts:*` or `read:*`)\n * - 2: exact string\n */\nexport function patternScore(pattern: string): number {\n if (pattern === WILDCARD) return 0;\n\n if (pattern.endsWith(':*')) return 1;\n\n return 2;\n}\n\n/**\n * Specificity: role(0|1) + resource(0|1|2) + action(0|1|2) = max 5.\n * Higher score = more specific = wins ties in priority.\n */\nfunction specificity<TAction extends string, TData>(rule: Readonly<NormalizedWardRule<TAction, TData>>): number {\n const roleScore = rule.role.includes(WILDCARD) ? 0 : 1;\n\n return roleScore + patternScore(rule.resource as string) + patternScore(rule.action as string);\n}\n\n// ---------------------------------------------------------------------------\n// Compilation\n// ---------------------------------------------------------------------------\n\nexport function compileEntry<TAction extends string, TData>(\n input: WardRule<TAction, TData>,\n index: number,\n): CompiledEntry<TAction, TData> {\n validateRuleInput(input, index);\n\n const rawRoles = Array.isArray(input.role) ? [...input.role] : [input.role];\n const roles: readonly string[] = Object.freeze([...new Set(rawRoles)]);\n\n const rule = Object.freeze({\n action: input.action,\n effect: input.effect,\n priority: input.priority ?? 0,\n resource: input.resource,\n role: roles,\n ...(input.when !== undefined ? { when: input.when } : {}),\n }) as Readonly<NormalizedWardRule<TAction, TData>>;\n\n const score = specificity(rule);\n const priority = rule.priority;\n const denyBonus: 0 | 1 = rule.effect === 'deny' ? 1 : 0;\n\n return { denyBonus, index, priority, roles, rule, score };\n}\n","import { WILDCARD } from './constants';\n\n/**\n * Matches a rule pattern against a concrete value string.\n * Works for both resource patterns and action patterns (action hierarchy).\n *\n * Matching rules (evaluated in order):\n * 1. `WILDCARD` (`*`) matches any value string.\n * 2. An exact string matches itself.\n * 3. A colon-namespaced pattern ending in `:*` (e.g. `posts:*` or `read:*`)\n * matches any string that starts with the given namespace prefix followed by `:`.\n * Example: `posts:*` matches `posts:123`, `posts:draft:42`, etc.\n * `read:*` matches `read:own`, `read:all`, etc.\n *\n * @example\n * matchesPattern('*', 'posts:123') // true — global wildcard\n * matchesPattern('posts', 'posts') // true — exact\n * matchesPattern('posts:*', 'posts:123') // true — namespace wildcard\n * matchesPattern('posts:*', 'comments:1') // false — different namespace\n * matchesPattern('posts:42', 'posts:42') // true — exact namespaced id\n * matchesPattern('read:*', 'read:own') // true — action namespace wildcard\n * matchesPattern('read:*', 'write:all') // false — different action namespace\n */\nexport function matchesPattern(pattern: string, value: string): boolean {\n if (pattern === WILDCARD) return true;\n\n if (pattern === value) return true;\n\n // Namespace wildcard: \"namespace:*\" matches \"namespace:anything\"\n if (pattern.endsWith(':*')) {\n const ns = pattern.slice(0, -1); // keep the trailing colon as prefix delimiter\n\n return value.startsWith(ns);\n }\n\n return false;\n}\n\n/**\n * Returns true if every concrete value that matches `narrow` also matches `broad`.\n * This is the coverage relation used for rule conflict detection.\n *\n * Pattern hierarchy (most to least permissive): `*` > `ns:*` > exact\n *\n * Unlike `matchesPattern(broad, narrow)` — which works accidentally — this\n * function explicitly documents and tests the coverage semantics so it remains\n * correct if new pattern syntax is added in the future.\n *\n * @example\n * patternCovers('*', 'posts:*') // true — * covers everything\n * patternCovers('posts:*', 'posts:123') // true — namespace covers exact\n * patternCovers('posts:*', 'posts:*') // true — same pattern covers itself\n * patternCovers('posts:*', '*') // false — namespace does not cover global wildcard\n * patternCovers('posts', 'posts:*') // false — exact does not cover namespace\n */\nexport function patternCovers(broad: string, narrow: string): boolean {\n // Global wildcard covers everything\n if (broad === WILDCARD) return true;\n\n // Nothing else covers global wildcard\n if (narrow === WILDCARD) return false;\n\n // Namespace wildcard: covers any value starting with the same namespace prefix,\n // including nested namespace wildcards (\"posts:*\" covers \"posts:sub:*\") and exact IDs.\n if (broad.endsWith(':*')) {\n return narrow.startsWith(broad.slice(0, -1));\n }\n\n // Exact only covers itself\n return broad === narrow;\n}\n","import type { CompiledEntry } from './_compile';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { WardConfigError, WardPredicateError } from './errors';\nimport { matchesPattern } from './resource';\nimport type { Principal, UserPrincipal, WardDecision } from './types';\n\n// ---------------------------------------------------------------------------\n// Principal validation\n// ---------------------------------------------------------------------------\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const p = input as Record<string, unknown>;\n\n if (typeof p.id !== 'string' || !p.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(p.roles) || p.roles.some((r) => typeof r !== 'string' || !(r as string).trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function validatePrincipal(principal: Principal): void {\n if (principal !== null) {\n assertUserPrincipal(principal);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Role matching\n// ---------------------------------------------------------------------------\n\n/** Returns true if the principal's role set intersects with the rule's roles. */\nexport function principalMatchesRoles(roles: readonly string[], principal: Principal): boolean {\n if (principal === null) {\n return roles.includes(ANONYMOUS);\n }\n\n if (roles.every((r) => r === ANONYMOUS)) return false;\n\n if (roles.includes(WILDCARD)) return true;\n\n return roles.some((role) => principal.roles.includes(role));\n}\n\n// ---------------------------------------------------------------------------\n// Rule matching\n// ---------------------------------------------------------------------------\n\n/**\n * Check whether a rule applies to a given request.\n *\n * @param action - Pass `undefined` to skip the action check (used by `rulesInScope`).\n * @param data - Data payload; passed to `when` predicates.\n * @param skipPredicate - When true, omit the `when` evaluation even when data is present.\n */\nexport function matchesRule<TAction extends string, TData>(\n entry: CompiledEntry<TAction, TData>,\n principal: Principal,\n resource: string,\n action: TAction | undefined,\n data: TData | undefined,\n skipPredicate = false,\n): boolean {\n if (!principalMatchesRoles(entry.roles, principal)) return false;\n\n if (!matchesPattern(entry.rule.resource as string, resource)) return false;\n\n if (action !== undefined && !matchesPattern(entry.rule.action as string, action)) return false;\n\n if (skipPredicate || !entry.rule.when) return true;\n\n if (principal === null) return false;\n\n try {\n const result: unknown = entry.rule.when({ data, principal });\n\n if (result instanceof Promise) {\n throw new TypeError(\n `Rule[${entry.index}] when() returned a Promise. Async predicates are not supported — use a synchronous predicate.`,\n );\n }\n\n return result as boolean;\n } catch (err) {\n throw new WardPredicateError(entry.index, err);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Winner selection\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if `challenger` would displace `current` as the pickWinner result.\n */\nexport function isOverriddenBy<TAction extends string, TData>(\n current: CompiledEntry<TAction, TData>,\n challenger: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n challenger.priority > current.priority ||\n (challenger.priority === current.priority && challenger.score > current.score) ||\n (challenger.priority === current.priority &&\n challenger.score === current.score &&\n challenger.denyBonus > current.denyBonus)\n );\n}\n\nexport function pickWinner<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n): CompiledEntry<TAction, TData> | undefined {\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, action, data)) continue;\n\n if (!winner || isOverriddenBy(winner, entry)) {\n winner = entry;\n }\n }\n\n return winner;\n}\n\nexport function toDecision<TAction extends string, TData>(\n winner: CompiledEntry<TAction, TData> | undefined,\n): WardDecision<TAction, TData> {\n if (!winner) return { allowed: false, reason: 'no-matching-rule' };\n\n if (winner.rule.effect === 'deny') {\n return {\n allowed: false,\n reason: 'explicit-deny',\n rule: winner.rule,\n };\n }\n\n return { allowed: true, rule: winner.rule };\n}\n","import type { CompiledEntry } from './_compile';\nimport { isOverriddenBy } from './_match';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { patternCovers } from './resource';\nimport type { WardConflict } from './types';\n\n// ---------------------------------------------------------------------------\n// Role coverage helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if every principal that satisfies `narrowRoles` also satisfies `broadRoles`.\n */\nfunction roleCoversRoles(broadRoles: readonly string[], narrowRoles: readonly string[]): boolean {\n const narrowHasAnonymous = narrowRoles.includes(ANONYMOUS);\n const narrowHasAuthenticated = narrowRoles.some((r) => r !== ANONYMOUS);\n\n if (narrowHasAnonymous && !broadRoles.includes(ANONYMOUS)) return false;\n\n if (narrowHasAuthenticated) {\n if (broadRoles.includes(WILDCARD)) return true;\n\n return narrowRoles.filter((r) => r !== ANONYMOUS).every((r) => broadRoles.includes(r));\n }\n\n return true;\n}\n\nfunction rolesSetsEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n\n const setA = new Set(a);\n\n return b.every((r) => setA.has(r));\n}\n\n/** Returns true if every request that triggers `narrow` would also trigger `broad`. */\nfunction ruleCovers<TAction extends string, TData>(\n broad: CompiledEntry<TAction, TData>,\n narrow: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n roleCoversRoles(broad.roles, narrow.roles) &&\n patternCovers(broad.rule.resource as string, narrow.rule.resource as string) &&\n patternCovers(broad.rule.action as string, narrow.rule.action as string)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Conflict detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detects conflicts between compiled entries.\n *\n * @complexity O(n²) in the number of rules. Scales to hundreds of rules\n * with negligible overhead; for very large auto-generated policies, use `maxConflicts`.\n */\nexport function computeConflicts<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n maxConflicts: number,\n): WardConflict<TAction, TData>[] {\n const conflicts: WardConflict<TAction, TData>[] = [];\n\n for (let i = 0; i < entries.length && conflicts.length < maxConflicts; i++) {\n for (let j = i + 1; j < entries.length && conflicts.length < maxConflicts; j++) {\n const a = entries[i];\n const b = entries[j];\n\n if (\n !a.rule.when &&\n !b.rule.when &&\n rolesSetsEqual(a.roles, b.roles) &&\n a.rule.resource === b.rule.resource &&\n a.rule.action === b.rule.action\n ) {\n conflicts.push({\n indexA: a.index,\n indexB: b.index,\n kind: 'duplicate',\n ruleA: a.rule,\n ruleB: b.rule,\n });\n continue;\n }\n\n const bCanOverrideA = isOverriddenBy(a, b);\n\n if (!bCanOverrideA && !a.rule.when && ruleCovers(a, b)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: b.index,\n shadowedRule: b.rule,\n shadowingIndex: a.index,\n shadowingRule: a.rule,\n });\n } else if (bCanOverrideA && !b.rule.when && ruleCovers(b, a)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: a.index,\n shadowedRule: a.rule,\n shadowingIndex: b.index,\n shadowingRule: b.rule,\n });\n }\n }\n }\n\n return conflicts;\n}\n","import type { CompiledEntry } from './_compile';\n\nimport { ANONYMOUS } from './constants';\n\nconst isDev = !(globalThis as { __WARD_PROD__?: boolean }).__WARD_PROD__;\n\n/** @internal @security Messages may include user-supplied role names. */\nfunction warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/ward] ${msg}`);\n}\n\n/**\n * Emit a development-only warning when an ANONYMOUS-role rule has a `when` predicate.\n * The predicate is skipped for unauthenticated principals, so the rule can never match\n * anonymous requests — likely a policy authoring mistake.\n */\nexport function warnAnonymousPredicates<TAction extends string, TData>(\n entries: readonly CompiledEntry<TAction, TData>[],\n): void {\n if (!isDev) return;\n\n for (const entry of entries) {\n if (!entry.rule.when) continue;\n\n const hasAnonymous = entry.roles.includes(ANONYMOUS);\n const hasOnlyAnonymous = hasAnonymous && entry.roles.length === 1;\n\n if (hasOnlyAnonymous) {\n warn(\n `Rule[${entry.index}] pairs role '${ANONYMOUS}' with a \\`when\\` predicate. ` +\n 'Predicates are skipped for unauthenticated principals, so this rule can never match anonymous requests.',\n );\n }\n }\n}\n","import type { CompiledEntry } from './_compile';\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { warnAnonymousPredicates } from './_dev';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\nimport type {\n BoundWard,\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n NormalizedWardRule,\n Principal,\n UserPrincipal,\n Ward,\n WardAllowedActionsInput,\n WardCheck,\n WardConflict,\n WardDecision,\n WardDecisionInput,\n WardDecisionResult,\n WardEvent,\n WardOptions,\n WardRule,\n WardRulesInScopeInput,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nconst warn = warnAnonymousPredicates;\n\n// ---------------------------------------------------------------------------\n// Shared loop cores (validation-free; used by both public API and forUser)\n// ---------------------------------------------------------------------------\n\nfunction coreAllowedActions<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data: TData | undefined,\n): TAction[] {\n const seen = new Set<TAction>();\n const result: TAction[] = [];\n\n for (const action of knownActions) {\n if (seen.has(action)) continue;\n\n seen.add(action);\n\n const winner = pickWinner(entries, principal, resource, action, data);\n\n if (winner?.rule.effect === 'allow') result.push(action);\n }\n\n return result;\n}\n\nfunction coreRulesInScope<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n data: TData | undefined,\n): NormalizedWardRule<TAction, TData>[] {\n const skipPredicate = data === undefined;\n const result: NormalizedWardRule<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule as NormalizedWardRule<TAction, TData>);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly (WardRule<TAction, TData> | readonly WardRule<TAction, TData>[])[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n if (options.maxConflicts !== undefined) {\n if (!Number.isFinite(options.maxConflicts) || options.maxConflicts < 0) {\n throw new WardConfigError('maxConflicts must be a finite non-negative number.');\n }\n }\n\n if (options.onConflict !== undefined && typeof options.onConflict !== 'function') {\n throw new WardConfigError('onConflict must be a function.');\n }\n\n const { maxConflicts = Infinity } = options;\n const tappers = new Set<(event: WardEvent<TAction, TData>) => void>();\n const flat: WardRule<TAction, TData>[] = [];\n\n for (const entry of rules) {\n if (Array.isArray(entry)) {\n for (const rule of entry) flat.push(rule);\n } else {\n flat.push(entry as WardRule<TAction, TData>);\n }\n }\n\n const entries = flat.map((rule, i) => compileEntry(rule, i));\n\n // Warn in development when an ANONYMOUS-role rule has a predicate.\n warn(entries);\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function emitTap(event: WardEvent<TAction, TData>): void {\n if (tappers.size === 0) return;\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {\n // Observability must not affect ward behavior.\n }\n }\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n emitTap({ action, data, decision, principal, resource });\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n // Request objects avoid call-site ambiguity between resource/action/data and\n // make later API growth additive instead of positional-breaking.\n\n function explain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[] {\n const { data, knownActions, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(\n input: WardRulesInScopeInput<TData>,\n ): ReadonlyArray<Readonly<NormalizedWardRule<TAction, TData>>> {\n const { data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (input: BoundWardAllowedActionsInput<TAction, TData>) =>\n coreAllowedActions(entries, snap, input.resource, input.knownActions, input.data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (input: BoundWardDecisionInput<TAction, TData>) =>\n evaluateAndLog(snap, input.resource, input.action, input.data),\n rulesInScope: (input: BoundWardRulesInScopeInput<TData>) =>\n coreRulesInScope(entries, snap, input.resource, input.data),\n trace: (input: BoundWardDecisionInput<TAction, TData>) =>\n trace({ action: input.action, data: input.data, principal: snap, resource: input.resource }),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n function tap(handler: (event: WardEvent<TAction, TData>) => void, opts?: { signal?: AbortSignal }): () => void {\n tappers.add(handler);\n\n const onAbort = () => tappers.delete(handler);\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n tappers.delete(handler);\n return () => {};\n }\n opts.signal.addEventListener('abort', onAbort, { once: true });\n }\n\n return () => {\n tappers.delete(handler);\n opts?.signal?.removeEventListener('abort', onAbort);\n };\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, tap, trace };\n}\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 return Object.freeze({\n action: rule.action,\n ...(rule.attributes === undefined ? {} : { attributes: snapshotAttributes(rule.attributes, `${at}.attributes`) }),\n ...(rule.condition === undefined ? {} : { condition: rule.condition }),\n effect: rule.effect,\n resource: rule.resource,\n }) as WardRule<TAction, TResource, TAttributes>;\n}\n","import { WILDCARD } from './constants';\n\n/**\n * Matches a rule pattern against a concrete value string.\n * Works for both resource patterns and action patterns (action hierarchy).\n *\n * Matching rules (evaluated in order):\n * 1. `WILDCARD` (`*`) matches any value string.\n * 2. An exact string matches itself.\n * 3. A colon-namespaced pattern ending in `:*` (e.g. `posts:*` or `read:*`)\n * matches any string that starts with the given namespace prefix followed by `:`.\n * Example: `posts:*` matches `posts:123`, `posts:draft:42`, etc.\n * `read:*` matches `read:own`, `read:all`, etc.\n *\n * @example\n * matchesPattern('*', 'posts:123') // true — global wildcard\n * matchesPattern('posts', 'posts') // true — exact\n * matchesPattern('posts:*', 'posts:123') // true — namespace wildcard\n * matchesPattern('posts:*', 'comments:1') // false — different namespace\n * matchesPattern('posts:42', 'posts:42') // true — exact namespaced id\n * matchesPattern('read:*', 'read:own') // true — action namespace wildcard\n * matchesPattern('read:*', 'write:all') // false — different action namespace\n */\nexport function matchesPattern(pattern: string, value: string): boolean {\n if (pattern === WILDCARD) return true;\n\n if (pattern === value) return true;\n\n // Namespace wildcard: \"namespace:*\" matches \"namespace:anything\"\n if (pattern.endsWith(':*')) {\n const ns = pattern.slice(0, -1); // keep the trailing colon as prefix delimiter\n\n return value.startsWith(ns);\n }\n\n return false;\n}\n\n/**\n * Returns true if every concrete value that matches `narrow` also matches `broad`.\n * This is the coverage relation used for rule conflict detection.\n *\n * Pattern hierarchy (most to least permissive): `*` > `ns:*` > exact\n *\n * Unlike `matchesPattern(broad, narrow)` — which works accidentally — this\n * function explicitly documents and tests the coverage semantics so it remains\n * correct if new pattern syntax is added in the future.\n *\n * @example\n * patternCovers('*', 'posts:*') // true — * covers everything\n * patternCovers('posts:*', 'posts:123') // true — namespace covers exact\n * patternCovers('posts:*', 'posts:*') // true — same pattern covers itself\n * patternCovers('posts:*', '*') // false — namespace does not cover global wildcard\n * patternCovers('posts', 'posts:*') // false — exact does not cover namespace\n */\nexport function patternCovers(broad: string, narrow: string): boolean {\n // Global wildcard covers everything\n if (broad === WILDCARD) return true;\n\n // Nothing else covers global wildcard\n if (narrow === WILDCARD) return false;\n\n // Namespace wildcard: covers any value starting with the same namespace prefix,\n // including nested namespace wildcards (\"posts:*\" covers \"posts:sub:*\") and exact IDs.\n if (broad.endsWith(':*')) {\n return narrow.startsWith(broad.slice(0, -1));\n }\n\n // Exact only covers itself\n return broad === narrow;\n}\n","import { snapshotAttributes } from './_compile';\nimport { WardConditionError, WardConfigError } from './errors';\nimport { matchesPattern } from './resource';\nimport type {\n Principal,\n UserPrincipal,\n WardAttributes,\n WardAttributeValue,\n WardDecisionInput,\n WardRule,\n} from './types';\n\n/** Asserts that `input` is a valid `UserPrincipal`. Throws with a clear message otherwise. */\nexport function assertUserPrincipal(input: unknown): asserts input is UserPrincipal {\n if (typeof input !== 'object' || !input) {\n throw new WardConfigError('Invalid principal: expected { id: string, roles: string[] }');\n }\n\n const principal = input as Record<string, unknown>;\n\n if (typeof principal.id !== 'string' || !principal.id.trim()) {\n throw new WardConfigError('Invalid principal: id must be a non-empty string');\n }\n\n if (!Array.isArray(principal.roles) || principal.roles.some((role) => typeof role !== 'string' || !role.trim())) {\n throw new WardConfigError('Invalid principal: roles must be an array of non-empty strings');\n }\n}\n\nexport function snapshotPrincipal(principal: Principal | undefined): Principal {\n if (principal === undefined || principal === null) return null;\n\n assertUserPrincipal(principal);\n\n return Object.freeze({\n ...(principal.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(principal.attributes, 'Principal.attributes') }),\n id: principal.id,\n roles: Object.freeze([...principal.roles]),\n });\n}\n\nexport function snapshotDecisionInput<\n TAction extends string,\n TResource extends string,\n TAttributes extends WardAttributes,\n>(\n input: WardDecisionInput<TAction, TResource, TAttributes>,\n): WardDecisionInput<TAction, TResource, TAttributes> & {\n principal: Principal;\n} {\n if (typeof input.action !== 'string' || !input.action.trim()) {\n throw new WardConfigError('Decision.action must be a non-empty string');\n }\n if (typeof input.resource !== 'string' || !input.resource.trim()) {\n throw new WardConfigError('Decision.resource must be a non-empty string');\n }\n\n return Object.freeze({\n action: input.action,\n ...(input.attributes === undefined\n ? {}\n : { attributes: snapshotAttributes(input.attributes, 'Decision.attributes') as TAttributes }),\n principal: snapshotPrincipal(input.principal),\n resource: input.resource,\n });\n}\n\nfunction deepEqual(left: WardAttributeValue, right: WardAttributeValue): boolean {\n if (Object.is(left, right)) return true;\n if (typeof left !== 'object' || typeof right !== 'object' || left === null || right === null) return false;\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((value, index) => deepEqual(value, right[index]))\n );\n }\n\n const leftRecord = left as Record<string, WardAttributeValue>;\n const rightRecord = right as Record<string, WardAttributeValue>;\n const leftKeys = Object.keys(leftRecord);\n const rightKeys = Object.keys(rightRecord);\n\n return (\n leftKeys.length === rightKeys.length &&\n leftKeys.every((key) => Object.hasOwn(rightRecord, key) && deepEqual(leftRecord[key], rightRecord[key]))\n );\n}\n\nexport function attributesMatch(expected: WardAttributes | undefined, actual: WardAttributes | undefined): boolean {\n if (!expected) return true;\n if (!actual) return false;\n\n return Object.keys(expected).every((key) => Object.hasOwn(actual, key) && deepEqual(expected[key], actual[key]));\n}\n\nfunction isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n (typeof value === 'object' && value !== null && 'then' in value && typeof value.then === 'function') ||\n (typeof value === 'function' && 'then' in value && typeof value.then === 'function')\n );\n}\n\nexport function ruleMatches<TAction extends string, TResource extends string, TAttributes extends WardAttributes>(\n rule: WardRule<TAction, TResource, TAttributes>,\n index: number,\n input: WardDecisionInput<TAction, TResource, TAttributes> & { principal: Principal },\n): boolean {\n if (!matchesPattern(rule.action, input.action)) return false;\n if (!matchesPattern(rule.resource, input.resource)) return false;\n if (!attributesMatch(rule.attributes, input.attributes)) return false;\n if (!rule.condition) return true;\n\n try {\n const result: unknown = rule.condition({ attributes: input.attributes, principal: input.principal });\n\n if (isThenable(result)) {\n throw new TypeError('Async conditions are not supported; return a boolean');\n }\n if (typeof result !== 'boolean') {\n throw new TypeError('Conditions must return a boolean');\n }\n\n return result;\n } catch (error) {\n throw new WardConditionError(index, error);\n }\n}\n","import { compileRule } from './_compile';\nimport { ruleMatches, snapshotDecisionInput, snapshotPrincipal } from './_match';\nimport { WardConfigError } from './errors';\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. Every key in its declarative `attributes` is present and equal in the\n * request's `attributes` (deep equality).\n * 4. 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 tappers = new Set<(event: WardEvent<TAction, TResource, TAttributes>) => void>();\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 if (tappers.size === 0) return;\n\n const event = Object.freeze({ decision, input, type: 'decision' as const });\n for (const tapper of tappers) {\n try {\n tapper(event);\n } catch {}\n }\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 knownActions: readonly TAction[];\n principal?: Principal;\n resource: TResource;\n }): TAction[] {\n const seen = new Set<TAction>();\n\n return input.knownActions.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 function tap(\n handler: (event: WardEvent<TAction, TResource, TAttributes>) => void,\n options?: { readonly signal?: AbortSignal },\n ): () => void {\n if (typeof handler !== 'function') throw new WardConfigError('tap handler must be a function');\n if (options?.signal?.aborted) return () => {};\n\n tappers.add(handler);\n const onAbort = () => tappers.delete(handler);\n options?.signal?.addEventListener('abort', onAbort, { once: true });\n\n return () => {\n tappers.delete(handler);\n options?.signal?.removeEventListener('abort', onAbort);\n };\n }\n\n return { allowedActions, checkAll, decide, forPrincipal, rules: compiled, tap };\n}\n","import { ANONYMOUS, WILDCARD } from './constants';\nimport type { WardAttributes, WardCondition, WardPattern, WardRule } from './types';\n\ntype RuleOptions<TAttributes extends WardAttributes> = Readonly<{\n when?: WardCondition<TAttributes>;\n}>;\n\nfunction roleCondition<TAttributes extends WardAttributes>(\n role: string | readonly string[],\n): WardCondition<TAttributes> {\n const roles = Object.freeze(Array.isArray(role) ? [...role] : [role]);\n\n return ({ principal }) =>\n roles.some((candidate) =>\n candidate === ANONYMOUS\n ? principal === null\n : candidate === WILDCARD\n ? principal !== null\n : principal?.roles.includes(candidate),\n );\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 matchesRole = roleCondition<TAttributes>(role);\n const condition = options?.when\n ? (input: Parameters<WardCondition<TAttributes>>[0]) => matchesRole(input) && options.when!(input)\n : matchesRole;\n\n return actions.map((action) => ({ action, condition, effect, resource }));\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 hasRole =\n <TAttributes extends WardAttributes = WardAttributes>(role: string): WardCondition<TAttributes> =>\n ({ principal }) =>\n principal?.roles.includes(role) ?? false;\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, hasRole, not, or, owns } as const;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vielzeug/ward",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Ordered authorization rules with immutable policies, typed decisions, and composable conditions",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/helmuthdu/vielzeug",
|
|
@@ -30,19 +30,19 @@
|
|
|
30
30
|
"registry": "https://registry.npmjs.org/"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "vite build && pnpm run build:bundle && pnpm run build:types",
|
|
33
|
+
"build": "vite build && pnpm --silent run build:bundle && pnpm --silent run build:types",
|
|
34
34
|
"build:bundle": "vite build --config vite.bundle.config.ts",
|
|
35
35
|
"build:types": "tsc -p tsconfig.declarations.json",
|
|
36
36
|
"fix": "biome check --write src",
|
|
37
37
|
"lint": "biome ci src",
|
|
38
|
-
"prepublishOnly": "pnpm run build",
|
|
38
|
+
"prepublishOnly": "pnpm --silent run build",
|
|
39
39
|
"preview": "vite preview",
|
|
40
40
|
"test": "vitest"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^26.
|
|
43
|
+
"@types/node": "^26.4.1",
|
|
44
44
|
"typescript": "^6.0.3",
|
|
45
|
-
"vite": "^8.2.
|
|
46
|
-
"vitest": "^
|
|
45
|
+
"vite": "^8.2.2",
|
|
46
|
+
"vitest": "^5.0.0"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/dist/_conflict.cjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=require("./constants.cjs"),t=require("./resource.cjs"),n=require("./_match.cjs");function r(t,n){let r=n.includes(e.ANONYMOUS),i=n.some(t=>t!==e.ANONYMOUS);return r&&!t.includes(`anonymous`)?!1:i?t.includes(`*`)?!0:n.filter(t=>t!==e.ANONYMOUS).every(e=>t.includes(e)):!0}function i(e,t){if(e.length!==t.length)return!1;let n=new Set(e);return t.every(e=>n.has(e))}function a(e,n){return r(e.roles,n.roles)&&t.patternCovers(e.rule.resource,n.rule.resource)&&t.patternCovers(e.rule.action,n.rule.action)}function o(e,t){let r=[];for(let o=0;o<e.length&&r.length<t;o++)for(let s=o+1;s<e.length&&r.length<t;s++){let t=e[o],c=e[s];if(!t.rule.when&&!c.rule.when&&i(t.roles,c.roles)&&t.rule.resource===c.rule.resource&&t.rule.action===c.rule.action){r.push({indexA:t.index,indexB:c.index,kind:`duplicate`,ruleA:t.rule,ruleB:c.rule});continue}let l=n.isOverriddenBy(t,c);!l&&!t.rule.when&&a(t,c)?r.push({kind:`shadowed`,shadowedIndex:c.index,shadowedRule:c.rule,shadowingIndex:t.index,shadowingRule:t.rule}):l&&!c.rule.when&&a(c,t)&&r.push({kind:`shadowed`,shadowedIndex:t.index,shadowedRule:t.rule,shadowingIndex:c.index,shadowingRule:c.rule})}return r}exports.computeConflicts=o;
|
|
2
|
-
//# sourceMappingURL=_conflict.cjs.map
|
package/dist/_conflict.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"_conflict.cjs","names":[],"sources":["../src/_conflict.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport { isOverriddenBy } from './_match';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { patternCovers } from './resource';\nimport type { WardConflict } from './types';\n\n// ---------------------------------------------------------------------------\n// Role coverage helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if every principal that satisfies `narrowRoles` also satisfies `broadRoles`.\n */\nfunction roleCoversRoles(broadRoles: readonly string[], narrowRoles: readonly string[]): boolean {\n const narrowHasAnonymous = narrowRoles.includes(ANONYMOUS);\n const narrowHasAuthenticated = narrowRoles.some((r) => r !== ANONYMOUS);\n\n if (narrowHasAnonymous && !broadRoles.includes(ANONYMOUS)) return false;\n\n if (narrowHasAuthenticated) {\n if (broadRoles.includes(WILDCARD)) return true;\n\n return narrowRoles.filter((r) => r !== ANONYMOUS).every((r) => broadRoles.includes(r));\n }\n\n return true;\n}\n\nfunction rolesSetsEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n\n const setA = new Set(a);\n\n return b.every((r) => setA.has(r));\n}\n\n/** Returns true if every request that triggers `narrow` would also trigger `broad`. */\nfunction ruleCovers<TAction extends string, TData>(\n broad: CompiledEntry<TAction, TData>,\n narrow: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n roleCoversRoles(broad.roles, narrow.roles) &&\n patternCovers(broad.rule.resource as string, narrow.rule.resource as string) &&\n patternCovers(broad.rule.action as string, narrow.rule.action as string)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Conflict detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detects conflicts between compiled entries.\n *\n * @complexity O(n²) in the number of rules. Scales to hundreds of rules\n * with negligible overhead; for very large auto-generated policies, use `maxConflicts`.\n */\nexport function computeConflicts<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n maxConflicts: number,\n): WardConflict<TAction, TData>[] {\n const conflicts: WardConflict<TAction, TData>[] = [];\n\n for (let i = 0; i < entries.length && conflicts.length < maxConflicts; i++) {\n for (let j = i + 1; j < entries.length && conflicts.length < maxConflicts; j++) {\n const a = entries[i];\n const b = entries[j];\n\n if (\n !a.rule.when &&\n !b.rule.when &&\n rolesSetsEqual(a.roles, b.roles) &&\n a.rule.resource === b.rule.resource &&\n a.rule.action === b.rule.action\n ) {\n conflicts.push({\n indexA: a.index,\n indexB: b.index,\n kind: 'duplicate',\n ruleA: a.rule,\n ruleB: b.rule,\n });\n continue;\n }\n\n const bCanOverrideA = isOverriddenBy(a, b);\n\n if (!bCanOverrideA && !a.rule.when && ruleCovers(a, b)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: b.index,\n shadowedRule: b.rule,\n shadowingIndex: a.index,\n shadowingRule: a.rule,\n });\n } else if (bCanOverrideA && !b.rule.when && ruleCovers(b, a)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: a.index,\n shadowedRule: a.rule,\n shadowingIndex: b.index,\n shadowingRule: b.rule,\n });\n }\n }\n }\n\n return conflicts;\n}\n"],"mappings":"yFAaA,SAAS,EAAgB,EAA+B,EAAyC,CAC/F,IAAM,EAAqB,EAAY,SAAS,EAAA,SAAS,EACnD,EAAyB,EAAY,KAAM,GAAM,IAAM,EAAA,SAAS,EAUtE,OARI,GAAsB,CAAC,EAAW,SAAA,WAAkB,EAAU,GAE9D,EACE,EAAW,SAAA,GAAiB,EAAU,GAEnC,EAAY,OAAQ,GAAM,IAAM,EAAA,SAAS,CAAC,CAAC,MAAO,GAAM,EAAW,SAAS,CAAC,CAAC,EAGhF,EACT,CAEA,SAAS,EAAe,EAAsB,EAA+B,CAC3E,GAAI,EAAE,SAAW,EAAE,OAAQ,MAAO,GAElC,IAAM,EAAO,IAAI,IAAI,CAAC,EAEtB,OAAO,EAAE,MAAO,GAAM,EAAK,IAAI,CAAC,CAAC,CACnC,CAGA,SAAS,EACP,EACA,EACS,CACT,OACE,EAAgB,EAAM,MAAO,EAAO,KAAK,GACzC,EAAA,cAAc,EAAM,KAAK,SAAoB,EAAO,KAAK,QAAkB,GAC3E,EAAA,cAAc,EAAM,KAAK,OAAkB,EAAO,KAAK,MAAgB,CAE3E,CAYA,SAAgB,EACd,EACA,EACgC,CAChC,IAAM,EAA4C,CAAC,EAEnD,IAAK,IAAI,EAAI,EAAG,EAAI,EAAQ,QAAU,EAAU,OAAS,EAAc,IACrE,IAAK,IAAI,EAAI,EAAI,EAAG,EAAI,EAAQ,QAAU,EAAU,OAAS,EAAc,IAAK,CAC9E,IAAM,EAAI,EAAQ,GACZ,EAAI,EAAQ,GAElB,GACE,CAAC,EAAE,KAAK,MACR,CAAC,EAAE,KAAK,MACR,EAAe,EAAE,MAAO,EAAE,KAAK,GAC/B,EAAE,KAAK,WAAa,EAAE,KAAK,UAC3B,EAAE,KAAK,SAAW,EAAE,KAAK,OACzB,CACA,EAAU,KAAK,CACb,OAAQ,EAAE,MACV,OAAQ,EAAE,MACV,KAAM,YACN,MAAO,EAAE,KACT,MAAO,EAAE,IACX,CAAC,EACD,QACF,CAEA,IAAM,EAAgB,EAAA,eAAe,EAAG,CAAC,EAErC,CAAC,GAAiB,CAAC,EAAE,KAAK,MAAQ,EAAW,EAAG,CAAC,EACnD,EAAU,KAAK,CACb,KAAM,WACN,cAAe,EAAE,MACjB,aAAc,EAAE,KAChB,eAAgB,EAAE,MAClB,cAAe,EAAE,IACnB,CAAC,EACQ,GAAiB,CAAC,EAAE,KAAK,MAAQ,EAAW,EAAG,CAAC,GACzD,EAAU,KAAK,CACb,KAAM,WACN,cAAe,EAAE,MACjB,aAAc,EAAE,KAChB,eAAgB,EAAE,MAClB,cAAe,EAAE,IACnB,CAAC,CAEL,CAGF,OAAO,CACT"}
|
package/dist/_conflict.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { CompiledEntry } from './_compile';
|
|
2
|
-
import type { WardConflict } from './types';
|
|
3
|
-
/**
|
|
4
|
-
* Detects conflicts between compiled entries.
|
|
5
|
-
*
|
|
6
|
-
* @complexity O(n²) in the number of rules. Scales to hundreds of rules
|
|
7
|
-
* with negligible overhead; for very large auto-generated policies, use `maxConflicts`.
|
|
8
|
-
*/
|
|
9
|
-
export declare function computeConflicts<TAction extends string, TData>(entries: CompiledEntry<TAction, TData>[], maxConflicts: number): WardConflict<TAction, TData>[];
|
|
10
|
-
//# sourceMappingURL=_conflict.d.ts.map
|
package/dist/_conflict.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"_conflict.d.ts","sourceRoot":"","sources":["../src/_conflict.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAgD5C;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EAC5D,OAAO,EAAE,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,EACxC,YAAY,EAAE,MAAM,GACnB,YAAY,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAgDhC"}
|
package/dist/_conflict.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { ANONYMOUS as e } from "./constants.js";
|
|
2
|
-
import { patternCovers as t } from "./resource.js";
|
|
3
|
-
import { isOverriddenBy as n } from "./_match.js";
|
|
4
|
-
//#region src/_conflict.ts
|
|
5
|
-
function r(t, n) {
|
|
6
|
-
let r = n.includes(e), i = n.some((t) => t !== e);
|
|
7
|
-
return r && !t.includes("anonymous") ? !1 : i ? t.includes("*") ? !0 : n.filter((t) => t !== e).every((e) => t.includes(e)) : !0;
|
|
8
|
-
}
|
|
9
|
-
function i(e, t) {
|
|
10
|
-
if (e.length !== t.length) return !1;
|
|
11
|
-
let n = new Set(e);
|
|
12
|
-
return t.every((e) => n.has(e));
|
|
13
|
-
}
|
|
14
|
-
function a(e, n) {
|
|
15
|
-
return r(e.roles, n.roles) && t(e.rule.resource, n.rule.resource) && t(e.rule.action, n.rule.action);
|
|
16
|
-
}
|
|
17
|
-
function o(e, t) {
|
|
18
|
-
let r = [];
|
|
19
|
-
for (let o = 0; o < e.length && r.length < t; o++) for (let s = o + 1; s < e.length && r.length < t; s++) {
|
|
20
|
-
let t = e[o], c = e[s];
|
|
21
|
-
if (!t.rule.when && !c.rule.when && i(t.roles, c.roles) && t.rule.resource === c.rule.resource && t.rule.action === c.rule.action) {
|
|
22
|
-
r.push({
|
|
23
|
-
indexA: t.index,
|
|
24
|
-
indexB: c.index,
|
|
25
|
-
kind: "duplicate",
|
|
26
|
-
ruleA: t.rule,
|
|
27
|
-
ruleB: c.rule
|
|
28
|
-
});
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
let l = n(t, c);
|
|
32
|
-
!l && !t.rule.when && a(t, c) ? r.push({
|
|
33
|
-
kind: "shadowed",
|
|
34
|
-
shadowedIndex: c.index,
|
|
35
|
-
shadowedRule: c.rule,
|
|
36
|
-
shadowingIndex: t.index,
|
|
37
|
-
shadowingRule: t.rule
|
|
38
|
-
}) : l && !c.rule.when && a(c, t) && r.push({
|
|
39
|
-
kind: "shadowed",
|
|
40
|
-
shadowedIndex: t.index,
|
|
41
|
-
shadowedRule: t.rule,
|
|
42
|
-
shadowingIndex: c.index,
|
|
43
|
-
shadowingRule: c.rule
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
return r;
|
|
47
|
-
}
|
|
48
|
-
//#endregion
|
|
49
|
-
export { o as computeConflicts };
|
|
50
|
-
|
|
51
|
-
//# sourceMappingURL=_conflict.js.map
|
package/dist/_conflict.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"_conflict.js","names":[],"sources":["../src/_conflict.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport { isOverriddenBy } from './_match';\nimport { ANONYMOUS, WILDCARD } from './constants';\nimport { patternCovers } from './resource';\nimport type { WardConflict } from './types';\n\n// ---------------------------------------------------------------------------\n// Role coverage helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true if every principal that satisfies `narrowRoles` also satisfies `broadRoles`.\n */\nfunction roleCoversRoles(broadRoles: readonly string[], narrowRoles: readonly string[]): boolean {\n const narrowHasAnonymous = narrowRoles.includes(ANONYMOUS);\n const narrowHasAuthenticated = narrowRoles.some((r) => r !== ANONYMOUS);\n\n if (narrowHasAnonymous && !broadRoles.includes(ANONYMOUS)) return false;\n\n if (narrowHasAuthenticated) {\n if (broadRoles.includes(WILDCARD)) return true;\n\n return narrowRoles.filter((r) => r !== ANONYMOUS).every((r) => broadRoles.includes(r));\n }\n\n return true;\n}\n\nfunction rolesSetsEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n\n const setA = new Set(a);\n\n return b.every((r) => setA.has(r));\n}\n\n/** Returns true if every request that triggers `narrow` would also trigger `broad`. */\nfunction ruleCovers<TAction extends string, TData>(\n broad: CompiledEntry<TAction, TData>,\n narrow: CompiledEntry<TAction, TData>,\n): boolean {\n return (\n roleCoversRoles(broad.roles, narrow.roles) &&\n patternCovers(broad.rule.resource as string, narrow.rule.resource as string) &&\n patternCovers(broad.rule.action as string, narrow.rule.action as string)\n );\n}\n\n// ---------------------------------------------------------------------------\n// Conflict detection\n// ---------------------------------------------------------------------------\n\n/**\n * Detects conflicts between compiled entries.\n *\n * @complexity O(n²) in the number of rules. Scales to hundreds of rules\n * with negligible overhead; for very large auto-generated policies, use `maxConflicts`.\n */\nexport function computeConflicts<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n maxConflicts: number,\n): WardConflict<TAction, TData>[] {\n const conflicts: WardConflict<TAction, TData>[] = [];\n\n for (let i = 0; i < entries.length && conflicts.length < maxConflicts; i++) {\n for (let j = i + 1; j < entries.length && conflicts.length < maxConflicts; j++) {\n const a = entries[i];\n const b = entries[j];\n\n if (\n !a.rule.when &&\n !b.rule.when &&\n rolesSetsEqual(a.roles, b.roles) &&\n a.rule.resource === b.rule.resource &&\n a.rule.action === b.rule.action\n ) {\n conflicts.push({\n indexA: a.index,\n indexB: b.index,\n kind: 'duplicate',\n ruleA: a.rule,\n ruleB: b.rule,\n });\n continue;\n }\n\n const bCanOverrideA = isOverriddenBy(a, b);\n\n if (!bCanOverrideA && !a.rule.when && ruleCovers(a, b)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: b.index,\n shadowedRule: b.rule,\n shadowingIndex: a.index,\n shadowingRule: a.rule,\n });\n } else if (bCanOverrideA && !b.rule.when && ruleCovers(b, a)) {\n conflicts.push({\n kind: 'shadowed',\n shadowedIndex: a.index,\n shadowedRule: a.rule,\n shadowingIndex: b.index,\n shadowingRule: b.rule,\n });\n }\n }\n }\n\n return conflicts;\n}\n"],"mappings":";;;;AAaA,SAAS,EAAgB,GAA+B,GAAyC;CAC/F,IAAM,IAAqB,EAAY,SAAS,CAAS,GACnD,IAAyB,EAAY,MAAM,MAAM,MAAM,CAAS;CAUtE,OARI,KAAsB,CAAC,EAAW,SAAA,WAAkB,IAAU,KAE9D,IACE,EAAW,SAAA,GAAiB,IAAU,KAEnC,EAAY,QAAQ,MAAM,MAAM,CAAS,CAAC,CAAC,OAAO,MAAM,EAAW,SAAS,CAAC,CAAC,IAGhF;AACT;AAEA,SAAS,EAAe,GAAsB,GAA+B;CAC3E,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAElC,IAAM,IAAO,IAAI,IAAI,CAAC;CAEtB,OAAO,EAAE,OAAO,MAAM,EAAK,IAAI,CAAC,CAAC;AACnC;AAGA,SAAS,EACP,GACA,GACS;CACT,OACE,EAAgB,EAAM,OAAO,EAAO,KAAK,KACzC,EAAc,EAAM,KAAK,UAAoB,EAAO,KAAK,QAAkB,KAC3E,EAAc,EAAM,KAAK,QAAkB,EAAO,KAAK,MAAgB;AAE3E;AAYA,SAAgB,EACd,GACA,GACgC;CAChC,IAAM,IAA4C,CAAC;CAEnD,KAAK,IAAI,IAAI,GAAG,IAAI,EAAQ,UAAU,EAAU,SAAS,GAAc,KACrE,KAAK,IAAI,IAAI,IAAI,GAAG,IAAI,EAAQ,UAAU,EAAU,SAAS,GAAc,KAAK;EAC9E,IAAM,IAAI,EAAQ,IACZ,IAAI,EAAQ;EAElB,IACE,CAAC,EAAE,KAAK,QACR,CAAC,EAAE,KAAK,QACR,EAAe,EAAE,OAAO,EAAE,KAAK,KAC/B,EAAE,KAAK,aAAa,EAAE,KAAK,YAC3B,EAAE,KAAK,WAAW,EAAE,KAAK,QACzB;GACA,EAAU,KAAK;IACb,QAAQ,EAAE;IACV,QAAQ,EAAE;IACV,MAAM;IACN,OAAO,EAAE;IACT,OAAO,EAAE;GACX,CAAC;GACD;EACF;EAEA,IAAM,IAAgB,EAAe,GAAG,CAAC;EAEzC,AAAI,CAAC,KAAiB,CAAC,EAAE,KAAK,QAAQ,EAAW,GAAG,CAAC,IACnD,EAAU,KAAK;GACb,MAAM;GACN,eAAe,EAAE;GACjB,cAAc,EAAE;GAChB,gBAAgB,EAAE;GAClB,eAAe,EAAE;EACnB,CAAC,IACQ,KAAiB,CAAC,EAAE,KAAK,QAAQ,EAAW,GAAG,CAAC,KACzD,EAAU,KAAK;GACb,MAAM;GACN,eAAe,EAAE;GACjB,cAAc,EAAE;GAChB,gBAAgB,EAAE;GAClB,eAAe,EAAE;EACnB,CAAC;CAEL;CAGF,OAAO;AACT"}
|
package/dist/_dev.cjs
DELETED
package/dist/_dev.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"_dev.cjs","names":[],"sources":["../src/_dev.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\n\nimport { ANONYMOUS } from './constants';\n\nconst isDev = !(globalThis as { __WARD_PROD__?: boolean }).__WARD_PROD__;\n\n/** @internal @security Messages may include user-supplied role names. */\nfunction warn(msg: string): void {\n if (isDev) console.warn(`[@vielzeug/ward] ${msg}`);\n}\n\n/**\n * Emit a development-only warning when an ANONYMOUS-role rule has a `when` predicate.\n * The predicate is skipped for unauthenticated principals, so the rule can never match\n * anonymous requests — likely a policy authoring mistake.\n */\nexport function warnAnonymousPredicates<TAction extends string, TData>(\n entries: readonly CompiledEntry<TAction, TData>[],\n): void {\n if (!isDev) return;\n\n for (const entry of entries) {\n if (!entry.rule.when) continue;\n\n const hasAnonymous = entry.roles.includes(ANONYMOUS);\n const hasOnlyAnonymous = hasAnonymous && entry.roles.length === 1;\n\n if (hasOnlyAnonymous) {\n warn(\n `Rule[${entry.index}] pairs role '${ANONYMOUS}' with a \\`when\\` predicate. ` +\n 'Predicates are skipped for unauthenticated principals, so this rule can never match anonymous requests.',\n );\n }\n }\n}\n"],"mappings":"AAgBA,SAAgB,EACd,EACM,CAgBR"}
|
package/dist/_dev.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { CompiledEntry } from './_compile';
|
|
2
|
-
/**
|
|
3
|
-
* Emit a development-only warning when an ANONYMOUS-role rule has a `when` predicate.
|
|
4
|
-
* The predicate is skipped for unauthenticated principals, so the rule can never match
|
|
5
|
-
* anonymous requests — likely a policy authoring mistake.
|
|
6
|
-
*/
|
|
7
|
-
export declare function warnAnonymousPredicates<TAction extends string, TData>(entries: readonly CompiledEntry<TAction, TData>[]): void;
|
|
8
|
-
//# sourceMappingURL=_dev.d.ts.map
|
package/dist/_dev.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"_dev.d.ts","sourceRoot":"","sources":["../src/_dev.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAWhD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,SAAS,MAAM,EAAE,KAAK,EACnE,OAAO,EAAE,SAAS,aAAa,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,GAChD,IAAI,CAgBN"}
|