@vielzeug/ward 1.0.5 → 2.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 CHANGED
@@ -45,27 +45,6 @@ bound.allowedActions({ resource: 'posts', knownActions: ['read', 'update', 'dele
45
45
  bound.explain({ resource: 'posts', action: 'update', data: { authorId: 'u2' } });
46
46
  ```
47
47
 
48
- ## Middleware Guards
49
-
50
- ```ts
51
- import { guardRequest, guardRequestWith } from '@vielzeug/ward';
52
-
53
- const direct = guardRequest({
54
- ward,
55
- principal,
56
- resource: 'posts',
57
- action: 'read',
58
- });
59
-
60
- const withExtractor = await guardRequestWith({
61
- ward,
62
- req,
63
- extractPrincipal: async (request) => request.user ?? null,
64
- resource: 'posts',
65
- action: 'read',
66
- });
67
- ```
68
-
69
48
  ## API Notes
70
49
 
71
50
  1. `explain()` and `trace()` take object inputs: `{ principal, resource, action, data? }`.
@@ -73,3 +52,4 @@ const withExtractor = await guardRequestWith({
73
52
  3. `rulesInScope()` takes `{ principal, resource, data? }`.
74
53
  4. `BoundWard` methods use object inputs without `principal`.
75
54
  5. `trace()` does not call the logger; `explain()` and `checkAll()` do.
55
+ 6. Use `explain()` directly at request boundaries instead of middleware wrappers.
package/dist/_dev.cjs ADDED
@@ -0,0 +1,2 @@
1
+ function e(e){}exports.warnAnonymousPredicates=e;
2
+ //# sourceMappingURL=_dev.cjs.map
@@ -0,0 +1 @@
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 ADDED
@@ -0,0 +1,8 @@
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
@@ -0,0 +1 @@
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"}
package/dist/_dev.js ADDED
@@ -0,0 +1,6 @@
1
+ //#region src/_dev.ts
2
+ function e(e) {}
3
+ //#endregion
4
+ export { e as warnAnonymousPredicates };
5
+
6
+ //# sourceMappingURL=_dev.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_dev.js","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,GACM,CAgBR"}
@@ -1 +1 @@
1
- {"version":3,"file":"builder.cjs","names":[],"sources":["../src/builder.ts"],"sourcesContent":["import type { WardPredicate, WardRule } from './types';\n\nimport { WILDCARD } from './constants';\n\n// ---------------------------------------------------------------------------\n// RuleOptions — shared options for allow/deny/ruleFor\n// ---------------------------------------------------------------------------\n\ntype RuleOptions<TData> = {\n priority?: number;\n when?: WardPredicate<TData>;\n};\n\n// ---------------------------------------------------------------------------\n// ruleFor — multi-action rule factory (low-level, effect as first arg)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action for a given effect, role(s), and resource.\n *\n * Prefer `allow()` or `deny()` for ergonomic rule authoring.\n *\n * @example\n * ```ts\n * ruleFor('allow', 'viewer', 'posts', ['read'])\n * ruleFor('deny', 'blocked', 'posts', ['read', 'update'])\n * ```\n */\nexport function ruleFor<TAction extends string = string, TData = unknown>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return actions.map((action) => ({\n action,\n effect,\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n resource,\n role,\n ...(options?.when !== undefined ? { when: options.when } : {}),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// allow / deny — ergonomic factories (R12)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action with `effect: 'allow'`.\n *\n * Reads naturally: \"allow editor to read/update posts\".\n *\n * @example\n * ```ts\n * allow('editor', 'posts', ['read', 'update'])\n * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })\n * ```\n */\nexport function allow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('allow', role, resource, actions, options);\n}\n\n/**\n * Creates one `WardRule` per action with `effect: 'deny'`.\n *\n * Reads naturally: \"deny blocked from reading posts\".\n *\n * @example\n * ```ts\n * deny('blocked', 'posts', ['read', 'update'])\n * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })\n * ```\n */\nexport function deny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('deny', role, resource, actions, options);\n}\n\n// ---------------------------------------------------------------------------\n// predicate — grouped predicate helpers (R10)\n// ---------------------------------------------------------------------------\n\n/**\n * Grouped predicate factories. Import as a namespace to avoid name collisions:\n * ```ts\n * import { predicate } from '@vielzeug/ward';\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })\n * ```\n */\nexport const predicate = {\n /**\n * Returns a `WardPredicate` that combines all given predicates with AND semantics —\n * all must return `true` for the rule to match.\n */\n and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.every((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that inverts the given predicate.\n */\n not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData> {\n return (ctx) => !pred(ctx);\n },\n\n /**\n * Returns a `WardPredicate` that combines all given predicates with OR semantics —\n * at least one must return `true` for the rule to match.\n */\n or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.some((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`. Use to express ownership constraints.\n *\n * Must be used with a rule that requires authentication (non-`ANONYMOUS` role).\n * Predicates only execute for authenticated principals — pairing `owns` with an\n * `ANONYMOUS`-role rule produces a rule that can never match because the predicate\n * is skipped for unauthenticated requests.\n *\n * @example\n * ```ts\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * ```\n */\n owns<TData = unknown>(attributeKey: keyof TData & string): WardPredicate<TData> {\n return ({ data, principal }) => {\n if (!data || typeof data !== 'object') return false;\n\n const record = data as Record<string, unknown>;\n\n if (!Object.hasOwn(record, attributeKey)) return false;\n\n return record[attributeKey] === principal.id;\n };\n },\n} as const;\n\n// ---------------------------------------------------------------------------\n// owns — top-level re-export for backward-compatible usage\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`.\n *\n * Also available as `predicate.owns()` when using the grouped namespace.\n */\nexport function owns<TData = unknown>(attributeKey: keyof TData & string): WardPredicate<TData> {\n return predicate.owns(attributeKey);\n}\n"],"mappings":"AA4BA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAQ,IAAK,IAAY,CAC9B,SACA,SACA,GAAI,GAAS,WAAa,IAAA,GAA6C,CAAC,EAAlC,CAAE,SAAU,EAAQ,QAAS,EACnE,WACA,OACA,GAAI,GAAS,OAAS,IAAA,GAAqC,CAAC,EAA1B,CAAE,KAAM,EAAQ,IAAK,CACzD,EAAE,CACJ,CAiBA,SAAgB,EACd,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAQ,QAAS,EAAM,EAAU,EAAS,CAAO,CAC1D,CAaA,SAAgB,EACd,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAQ,OAAQ,EAAM,EAAU,EAAS,CAAO,CACzD,CAcA,IAAa,EAAY,CAKvB,IAAqB,GAAG,EAAqD,CAC3E,MAAQ,IAAQ,EAAM,MAAO,GAAM,EAAE,CAAG,CAAC,CAC3C,EAKA,IAAqB,EAAkD,CACrE,MAAQ,IAAQ,CAAC,EAAK,CAAG,CAC3B,EAMA,GAAoB,GAAG,EAAqD,CAC1E,MAAQ,IAAQ,EAAM,KAAM,GAAM,EAAE,CAAG,CAAC,CAC1C,EAgBA,KAAsB,EAA0D,CAC9E,OAAQ,CAAE,OAAM,eAAgB,CAC9B,GAAI,CAAC,GAAQ,OAAO,GAAS,SAAU,MAAO,GAE9C,IAAM,EAAS,EAIf,OAFK,OAAO,OAAO,EAAQ,CAAY,EAEhC,EAAO,KAAkB,EAAU,GAFO,EAGnD,CACF,CACF,EAYA,SAAgB,EAAsB,EAA0D,CAC9F,OAAO,EAAU,KAAK,CAAY,CACpC"}
1
+ {"version":3,"file":"builder.cjs","names":[],"sources":["../src/builder.ts"],"sourcesContent":["import type { WardPredicate, WardRule } from './types';\n\nimport { WILDCARD } from './constants';\n\n// ---------------------------------------------------------------------------\n// RuleOptions — shared options for allow/deny/ruleFor\n// ---------------------------------------------------------------------------\n\ntype RuleOptions<TData> = {\n priority?: number;\n when?: WardPredicate<TData>;\n};\n\n// ---------------------------------------------------------------------------\n// ruleFor — multi-action rule factory (low-level, effect as first arg)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action for a given effect, role(s), and resource.\n *\n * Prefer `allow()` or `deny()` for ergonomic rule authoring.\n *\n * @example\n * ```ts\n * ruleFor('allow', 'viewer', 'posts', ['read'])\n * ruleFor('deny', 'blocked', 'posts', ['read', 'update'])\n * ```\n */\nexport function ruleFor<TAction extends string = string, TData = unknown>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return actions.map((action) => ({\n action,\n effect,\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n resource,\n role,\n ...(options?.when !== undefined ? { when: options.when } : {}),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// allow / deny — ergonomic factories (R12)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action with `effect: 'allow'`.\n *\n * Reads naturally: \"allow editor to read/update posts\".\n *\n * @example\n * ```ts\n * allow('editor', 'posts', ['read', 'update'])\n * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })\n * ```\n */\nexport function allow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('allow', role, resource, actions, options);\n}\n\n/**\n * Creates one `WardRule` per action with `effect: 'deny'`.\n *\n * Reads naturally: \"deny blocked from reading posts\".\n *\n * @example\n * ```ts\n * deny('blocked', 'posts', ['read', 'update'])\n * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })\n * ```\n */\nexport function deny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('deny', role, resource, actions, options);\n}\n\n// ---------------------------------------------------------------------------\n// predicate — grouped predicate helpers (R10)\n// ---------------------------------------------------------------------------\n\n/**\n * Grouped predicate factories. Import as a namespace to avoid name collisions:\n * ```ts\n * import { predicate } from '@vielzeug/ward';\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })\n * ```\n */\nexport const predicate = {\n /**\n * Returns a `WardPredicate` that combines all given predicates with AND semantics —\n * all must return `true` for the rule to match.\n */\n and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.every((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that inverts the given predicate.\n */\n not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData> {\n return (ctx) => !pred(ctx);\n },\n\n /**\n * Returns a `WardPredicate` that combines all given predicates with OR semantics —\n * at least one must return `true` for the rule to match.\n */\n or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.some((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`. Use to express ownership constraints.\n *\n * Must be used with a rule that requires authentication (non-`ANONYMOUS` role).\n * Predicates only execute for authenticated principals — pairing `owns` with an\n * `ANONYMOUS`-role rule produces a rule that can never match because the predicate\n * is skipped for unauthenticated requests.\n *\n * @example\n * ```ts\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * ```\n */\n owns<TData = unknown>(\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\n// ---------------------------------------------------------------------------\n// owns — top-level re-export for backward-compatible usage\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`.\n *\n * Also available as `predicate.owns()` when using the grouped namespace.\n */\nexport function owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n): WardPredicate<TData> {\n return predicate.owns(attributeKey);\n}\n"],"mappings":"AA4BA,SAAgB,EACd,EACA,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAQ,IAAK,IAAY,CAC9B,SACA,SACA,GAAI,GAAS,WAAa,IAAA,GAA6C,CAAC,EAAlC,CAAE,SAAU,EAAQ,QAAS,EACnE,WACA,OACA,GAAI,GAAS,OAAS,IAAA,GAAqC,CAAC,EAA1B,CAAE,KAAM,EAAQ,IAAK,CACzD,EAAE,CACJ,CAiBA,SAAgB,EACd,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAQ,QAAS,EAAM,EAAU,EAAS,CAAO,CAC1D,CAaA,SAAgB,EACd,EACA,EACA,EACA,EAC4B,CAC5B,OAAO,EAAQ,OAAQ,EAAM,EAAU,EAAS,CAAO,CACzD,CAcA,IAAa,EAAY,CAKvB,IAAqB,GAAG,EAAqD,CAC3E,MAAQ,IAAQ,EAAM,MAAO,GAAM,EAAE,CAAG,CAAC,CAC3C,EAKA,IAAqB,EAAkD,CACrE,MAAQ,IAAQ,CAAC,EAAK,CAAG,CAC3B,EAMA,GAAoB,GAAG,EAAqD,CAC1E,MAAQ,IAAQ,EAAM,KAAM,GAAM,EAAE,CAAG,CAAC,CAC1C,EAgBA,KACE,EACsB,CACtB,OAAQ,CAAE,OAAM,eAAgB,CAC9B,GAAI,CAAC,GAAQ,OAAO,GAAS,SAAU,MAAO,GAE9C,IAAM,EAAS,EAIf,OAFK,OAAO,OAAO,EAAQ,CAAY,EAEhC,EAAO,KAAkB,EAAU,GAFO,EAGnD,CACF,CACF,EAYA,SAAgB,EACd,EACsB,CACtB,OAAO,EAAU,KAAK,CAAY,CACpC"}
package/dist/builder.d.ts CHANGED
@@ -15,7 +15,7 @@ type RuleOptions<TData> = {
15
15
  * ruleFor('deny', 'blocked', 'posts', ['read', 'update'])
16
16
  * ```
17
17
  */
18
- export declare function ruleFor<TAction extends string = string, TData = unknown>(effect: 'allow' | 'deny', role: string | readonly string[], resource: string | typeof WILDCARD, actions: readonly (TAction | typeof WILDCARD)[], options?: RuleOptions<TData>): WardRule<TAction, TData>[];
18
+ export declare function ruleFor<TAction extends string = string, TData = unknown>(effect: 'allow' | 'deny', role: string | readonly string[], resource: string | typeof WILDCARD, actions: readonly (TAction | NoInfer<typeof WILDCARD>)[], options?: RuleOptions<TData>): WardRule<TAction, TData>[];
19
19
  /**
20
20
  * Creates one `WardRule` per action with `effect: 'allow'`.
21
21
  *
@@ -27,7 +27,7 @@ export declare function ruleFor<TAction extends string = string, TData = unknown
27
27
  * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })
28
28
  * ```
29
29
  */
30
- export declare function allow<TAction extends string = string, TData = unknown>(role: string | readonly string[], resource: string | typeof WILDCARD, actions: readonly (TAction | typeof WILDCARD)[], options?: RuleOptions<TData>): WardRule<TAction, TData>[];
30
+ export declare function allow<TAction extends string = string, TData = unknown>(role: string | readonly string[], resource: string | typeof WILDCARD, actions: readonly (TAction | NoInfer<typeof WILDCARD>)[], options?: RuleOptions<TData>): WardRule<TAction, TData>[];
31
31
  /**
32
32
  * Creates one `WardRule` per action with `effect: 'deny'`.
33
33
  *
@@ -39,7 +39,7 @@ export declare function allow<TAction extends string = string, TData = unknown>(
39
39
  * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })
40
40
  * ```
41
41
  */
42
- export declare function deny<TAction extends string = string, TData = unknown>(role: string | readonly string[], resource: string | typeof WILDCARD, actions: readonly (TAction | typeof WILDCARD)[], options?: RuleOptions<TData>): WardRule<TAction, TData>[];
42
+ export declare function deny<TAction extends string = string, TData = unknown>(role: string | readonly string[], resource: string | typeof WILDCARD, actions: readonly (TAction | NoInfer<typeof WILDCARD>)[], options?: RuleOptions<TData>): WardRule<TAction, TData>[];
43
43
  /**
44
44
  * Grouped predicate factories. Import as a namespace to avoid name collisions:
45
45
  * ```ts
@@ -77,7 +77,7 @@ export declare const predicate: {
77
77
  * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })
78
78
  * ```
79
79
  */
80
- readonly owns: <TData = unknown>(attributeKey: keyof TData & string) => WardPredicate<TData>;
80
+ readonly owns: <TData = unknown>(attributeKey: [keyof TData] extends [never] ? string : keyof TData & string) => WardPredicate<TData>;
81
81
  };
82
82
  /**
83
83
  * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field
@@ -85,6 +85,6 @@ export declare const predicate: {
85
85
  *
86
86
  * Also available as `predicate.owns()` when using the grouped namespace.
87
87
  */
88
- export declare function owns<TData = unknown>(attributeKey: keyof TData & string): WardPredicate<TData>;
88
+ export declare function owns<TData = unknown>(attributeKey: [keyof TData] extends [never] ? string : keyof TData & string): WardPredicate<TData>;
89
89
  export {};
90
90
  //# sourceMappingURL=builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEvD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAMvC,KAAK,WAAW,CAAC,KAAK,IAAI;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;CAC7B,CAAC;AAMF;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACtE,MAAM,EAAE,OAAO,GAAG,MAAM,EACxB,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,QAAQ,CAAC,EAAE,EAC/C,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAS5B;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACpE,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,QAAQ,CAAC,EAAE,EAC/C,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAE5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACnE,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,QAAQ,CAAC,EAAE,EAC/C,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAE5B;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS;IACpB;;;OAGG;mBACC,KAAK,sBAAsB,aAAa,CAAC,KAAK,CAAC,EAAE,KAAG,aAAa,CAAC,KAAK,CAAC;IAI5E;;OAEG;mBACC,KAAK,kBAAkB,aAAa,CAAC,KAAK,CAAC,KAAG,aAAa,CAAC,KAAK,CAAC;IAItE;;;OAGG;kBACA,KAAK,sBAAsB,aAAa,CAAC,KAAK,CAAC,EAAE,KAAG,aAAa,CAAC,KAAK,CAAC;IAI3E;;;;;;;;;;;;;OAaG;oBACE,KAAK,0BAA0B,MAAM,KAAK,GAAG,MAAM,KAAG,aAAa,CAAC,KAAK,CAAC;CAWvE,CAAC;AAMX;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,KAAK,GAAG,OAAO,EAAE,YAAY,EAAE,MAAM,KAAK,GAAG,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAE9F"}
1
+ {"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEvD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAMvC,KAAK,WAAW,CAAC,KAAK,IAAI;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;CAC7B,CAAC;AAMF;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACtE,MAAM,EAAE,OAAO,GAAG,MAAM,EACxB,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,QAAQ,CAAC,CAAC,EAAE,EACxD,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAS5B;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACpE,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,QAAQ,CAAC,CAAC,EAAE,EACxD,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAE5B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACnE,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAChC,QAAQ,EAAE,MAAM,GAAG,OAAO,QAAQ,EAClC,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,QAAQ,CAAC,CAAC,EAAE,EACxD,OAAO,CAAC,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,CAE5B;AAMD;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS;IACpB;;;OAGG;mBACC,KAAK,sBAAsB,aAAa,CAAC,KAAK,CAAC,EAAE,KAAG,aAAa,CAAC,KAAK,CAAC;IAI5E;;OAEG;mBACC,KAAK,kBAAkB,aAAa,CAAC,KAAK,CAAC,KAAG,aAAa,CAAC,KAAK,CAAC;IAItE;;;OAGG;kBACA,KAAK,sBAAsB,aAAa,CAAC,KAAK,CAAC,EAAE,KAAG,aAAa,CAAC,KAAK,CAAC;IAI3E;;;;;;;;;;;;;OAaG;oBACE,KAAK,0BACM,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM,KAC1E,aAAa,CAAC,KAAK,CAAC;CAWf,CAAC;AAMX;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,KAAK,GAAG,OAAO,EAClC,YAAY,EAAE,CAAC,MAAM,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM,GAC1E,aAAa,CAAC,KAAK,CAAC,CAEtB"}
@@ -1 +1 @@
1
- {"version":3,"file":"builder.js","names":[],"sources":["../src/builder.ts"],"sourcesContent":["import type { WardPredicate, WardRule } from './types';\n\nimport { WILDCARD } from './constants';\n\n// ---------------------------------------------------------------------------\n// RuleOptions — shared options for allow/deny/ruleFor\n// ---------------------------------------------------------------------------\n\ntype RuleOptions<TData> = {\n priority?: number;\n when?: WardPredicate<TData>;\n};\n\n// ---------------------------------------------------------------------------\n// ruleFor — multi-action rule factory (low-level, effect as first arg)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action for a given effect, role(s), and resource.\n *\n * Prefer `allow()` or `deny()` for ergonomic rule authoring.\n *\n * @example\n * ```ts\n * ruleFor('allow', 'viewer', 'posts', ['read'])\n * ruleFor('deny', 'blocked', 'posts', ['read', 'update'])\n * ```\n */\nexport function ruleFor<TAction extends string = string, TData = unknown>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return actions.map((action) => ({\n action,\n effect,\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n resource,\n role,\n ...(options?.when !== undefined ? { when: options.when } : {}),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// allow / deny — ergonomic factories (R12)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action with `effect: 'allow'`.\n *\n * Reads naturally: \"allow editor to read/update posts\".\n *\n * @example\n * ```ts\n * allow('editor', 'posts', ['read', 'update'])\n * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })\n * ```\n */\nexport function allow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('allow', role, resource, actions, options);\n}\n\n/**\n * Creates one `WardRule` per action with `effect: 'deny'`.\n *\n * Reads naturally: \"deny blocked from reading posts\".\n *\n * @example\n * ```ts\n * deny('blocked', 'posts', ['read', 'update'])\n * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })\n * ```\n */\nexport function deny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | typeof WILDCARD)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('deny', role, resource, actions, options);\n}\n\n// ---------------------------------------------------------------------------\n// predicate — grouped predicate helpers (R10)\n// ---------------------------------------------------------------------------\n\n/**\n * Grouped predicate factories. Import as a namespace to avoid name collisions:\n * ```ts\n * import { predicate } from '@vielzeug/ward';\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })\n * ```\n */\nexport const predicate = {\n /**\n * Returns a `WardPredicate` that combines all given predicates with AND semantics —\n * all must return `true` for the rule to match.\n */\n and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.every((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that inverts the given predicate.\n */\n not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData> {\n return (ctx) => !pred(ctx);\n },\n\n /**\n * Returns a `WardPredicate` that combines all given predicates with OR semantics —\n * at least one must return `true` for the rule to match.\n */\n or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.some((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`. Use to express ownership constraints.\n *\n * Must be used with a rule that requires authentication (non-`ANONYMOUS` role).\n * Predicates only execute for authenticated principals — pairing `owns` with an\n * `ANONYMOUS`-role rule produces a rule that can never match because the predicate\n * is skipped for unauthenticated requests.\n *\n * @example\n * ```ts\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * ```\n */\n owns<TData = unknown>(attributeKey: keyof TData & string): WardPredicate<TData> {\n return ({ data, principal }) => {\n if (!data || typeof data !== 'object') return false;\n\n const record = data as Record<string, unknown>;\n\n if (!Object.hasOwn(record, attributeKey)) return false;\n\n return record[attributeKey] === principal.id;\n };\n },\n} as const;\n\n// ---------------------------------------------------------------------------\n// owns — top-level re-export for backward-compatible usage\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`.\n *\n * Also available as `predicate.owns()` when using the grouped namespace.\n */\nexport function owns<TData = unknown>(attributeKey: keyof TData & string): WardPredicate<TData> {\n return predicate.owns(attributeKey);\n}\n"],"mappings":";AA4BA,SAAgB,EACd,GACA,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,KAAK,OAAY;EAC9B;EACA;EACA,GAAI,GAAS,aAAa,KAAA,IAA6C,CAAC,IAAlC,EAAE,UAAU,EAAQ,SAAS;EACnE;EACA;EACA,GAAI,GAAS,SAAS,KAAA,IAAqC,CAAC,IAA1B,EAAE,MAAM,EAAQ,KAAK;CACzD,EAAE;AACJ;AAiBA,SAAgB,EACd,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,SAAS,GAAM,GAAU,GAAS,CAAO;AAC1D;AAaA,SAAgB,EACd,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,QAAQ,GAAM,GAAU,GAAS,CAAO;AACzD;AAcA,IAAa,IAAY;CAKvB,IAAqB,GAAG,GAAqD;EAC3E,QAAQ,MAAQ,EAAM,OAAO,MAAM,EAAE,CAAG,CAAC;CAC3C;CAKA,IAAqB,GAAkD;EACrE,QAAQ,MAAQ,CAAC,EAAK,CAAG;CAC3B;CAMA,GAAoB,GAAG,GAAqD;EAC1E,QAAQ,MAAQ,EAAM,MAAM,MAAM,EAAE,CAAG,CAAC;CAC1C;CAgBA,KAAsB,GAA0D;EAC9E,QAAQ,EAAE,SAAM,mBAAgB;GAC9B,IAAI,CAAC,KAAQ,OAAO,KAAS,UAAU,OAAO;GAE9C,IAAM,IAAS;GAIf,OAFK,OAAO,OAAO,GAAQ,CAAY,IAEhC,EAAO,OAAkB,EAAU,KAFO;EAGnD;CACF;AACF;AAYA,SAAgB,EAAsB,GAA0D;CAC9F,OAAO,EAAU,KAAK,CAAY;AACpC"}
1
+ {"version":3,"file":"builder.js","names":[],"sources":["../src/builder.ts"],"sourcesContent":["import type { WardPredicate, WardRule } from './types';\n\nimport { WILDCARD } from './constants';\n\n// ---------------------------------------------------------------------------\n// RuleOptions — shared options for allow/deny/ruleFor\n// ---------------------------------------------------------------------------\n\ntype RuleOptions<TData> = {\n priority?: number;\n when?: WardPredicate<TData>;\n};\n\n// ---------------------------------------------------------------------------\n// ruleFor — multi-action rule factory (low-level, effect as first arg)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action for a given effect, role(s), and resource.\n *\n * Prefer `allow()` or `deny()` for ergonomic rule authoring.\n *\n * @example\n * ```ts\n * ruleFor('allow', 'viewer', 'posts', ['read'])\n * ruleFor('deny', 'blocked', 'posts', ['read', 'update'])\n * ```\n */\nexport function ruleFor<TAction extends string = string, TData = unknown>(\n effect: 'allow' | 'deny',\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return actions.map((action) => ({\n action,\n effect,\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n resource,\n role,\n ...(options?.when !== undefined ? { when: options.when } : {}),\n }));\n}\n\n// ---------------------------------------------------------------------------\n// allow / deny — ergonomic factories (R12)\n// ---------------------------------------------------------------------------\n\n/**\n * Creates one `WardRule` per action with `effect: 'allow'`.\n *\n * Reads naturally: \"allow editor to read/update posts\".\n *\n * @example\n * ```ts\n * allow('editor', 'posts', ['read', 'update'])\n * allow(['editor', 'admin'], 'posts:*', ['read', 'update'], { when: predicate.owns('authorId') })\n * ```\n */\nexport function allow<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('allow', role, resource, actions, options);\n}\n\n/**\n * Creates one `WardRule` per action with `effect: 'deny'`.\n *\n * Reads naturally: \"deny blocked from reading posts\".\n *\n * @example\n * ```ts\n * deny('blocked', 'posts', ['read', 'update'])\n * deny('guest', WILDCARD, [WILDCARD], { priority: 10 })\n * ```\n */\nexport function deny<TAction extends string = string, TData = unknown>(\n role: string | readonly string[],\n resource: string | typeof WILDCARD,\n actions: readonly (TAction | NoInfer<typeof WILDCARD>)[],\n options?: RuleOptions<TData>,\n): WardRule<TAction, TData>[] {\n return ruleFor('deny', role, resource, actions, options);\n}\n\n// ---------------------------------------------------------------------------\n// predicate — grouped predicate helpers (R10)\n// ---------------------------------------------------------------------------\n\n/**\n * Grouped predicate factories. Import as a namespace to avoid name collisions:\n * ```ts\n * import { predicate } from '@vielzeug/ward';\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * allow('user', 'posts:*', ['read'], { when: predicate.and(predicate.owns('authorId'), myPred) })\n * ```\n */\nexport const predicate = {\n /**\n * Returns a `WardPredicate` that combines all given predicates with AND semantics —\n * all must return `true` for the rule to match.\n */\n and<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.every((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that inverts the given predicate.\n */\n not<TData = unknown>(pred: WardPredicate<TData>): WardPredicate<TData> {\n return (ctx) => !pred(ctx);\n },\n\n /**\n * Returns a `WardPredicate` that combines all given predicates with OR semantics —\n * at least one must return `true` for the rule to match.\n */\n or<TData = unknown>(...preds: WardPredicate<TData>[]): WardPredicate<TData> {\n return (ctx) => preds.some((p) => p(ctx));\n },\n\n /**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`. Use to express ownership constraints.\n *\n * Must be used with a rule that requires authentication (non-`ANONYMOUS` role).\n * Predicates only execute for authenticated principals — pairing `owns` with an\n * `ANONYMOUS`-role rule produces a rule that can never match because the predicate\n * is skipped for unauthenticated requests.\n *\n * @example\n * ```ts\n * allow('editor', 'posts:*', ['update'], { when: predicate.owns('authorId') })\n * ```\n */\n owns<TData = unknown>(\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\n// ---------------------------------------------------------------------------\n// owns — top-level re-export for backward-compatible usage\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a `WardPredicate` that checks whether the data object's `attributeKey` field\n * matches the principal's `id`.\n *\n * Also available as `predicate.owns()` when using the grouped namespace.\n */\nexport function owns<TData = unknown>(\n attributeKey: [keyof TData] extends [never] ? string : keyof TData & string,\n): WardPredicate<TData> {\n return predicate.owns(attributeKey);\n}\n"],"mappings":";AA4BA,SAAgB,EACd,GACA,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,KAAK,OAAY;EAC9B;EACA;EACA,GAAI,GAAS,aAAa,KAAA,IAA6C,CAAC,IAAlC,EAAE,UAAU,EAAQ,SAAS;EACnE;EACA;EACA,GAAI,GAAS,SAAS,KAAA,IAAqC,CAAC,IAA1B,EAAE,MAAM,EAAQ,KAAK;CACzD,EAAE;AACJ;AAiBA,SAAgB,EACd,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,SAAS,GAAM,GAAU,GAAS,CAAO;AAC1D;AAaA,SAAgB,EACd,GACA,GACA,GACA,GAC4B;CAC5B,OAAO,EAAQ,QAAQ,GAAM,GAAU,GAAS,CAAO;AACzD;AAcA,IAAa,IAAY;CAKvB,IAAqB,GAAG,GAAqD;EAC3E,QAAQ,MAAQ,EAAM,OAAO,MAAM,EAAE,CAAG,CAAC;CAC3C;CAKA,IAAqB,GAAkD;EACrE,QAAQ,MAAQ,CAAC,EAAK,CAAG;CAC3B;CAMA,GAAoB,GAAG,GAAqD;EAC1E,QAAQ,MAAQ,EAAM,MAAM,MAAM,EAAE,CAAG,CAAC;CAC1C;CAgBA,KACE,GACsB;EACtB,QAAQ,EAAE,SAAM,mBAAgB;GAC9B,IAAI,CAAC,KAAQ,OAAO,KAAS,UAAU,OAAO;GAE9C,IAAM,IAAS;GAIf,OAFK,OAAO,OAAO,GAAQ,CAAY,IAEhC,EAAO,OAAkB,EAAU,KAFO;EAGnD;CACF;AACF;AAYA,SAAgB,EACd,GACsB;CACtB,OAAO,EAAU,KAAK,CAAY;AACpC"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all ward errors. Use `instanceof WardError` to catch any ward-originated error. */\nexport class WardError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is WardError {\n return err instanceof WardError;\n }\n}\n\n/** Thrown when a rule definition or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/**\n * Thrown when a `when` predicate in a ward rule throws an exception.\n *\n * Catch this to distinguish predicate failures from other errors:\n * ```ts\n * try {\n * ward.explain(principal, resource, action, data);\n * } catch (e) {\n * if (e instanceof WardPredicateError) {\n * console.error(`Predicate in Rule[${e.ruleIndex}] threw`, e.cause);\n * }\n * }\n * ```\n */\nexport class WardPredicateError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n\n super(`Rule[${ruleIndex}] threw: ${msg}`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n"],"mappings":"AACA,IAAa,EAAb,MAAa,UAAkB,KAAM,CACnC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,IAAI,OAAO,KACvB,OAAO,eAAe,KAAM,IAAI,OAAO,SAAS,CAClD,CAEA,OAAO,GAAG,EAAgC,CACxC,OAAO,aAAe,CACxB,CACF,EAGa,EAAb,cAAqC,CAAU,CAAC,EAgBnC,EAAb,cAAwC,CAAU,CAChD,UAEA,YAAY,EAAmB,EAAgB,CAC7C,IAAM,EAAM,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EAEjE,MAAM,QAAQ,EAAU,WAAW,IAAO,CAAE,OAAM,CAAC,EACnD,KAAK,UAAY,CACnB,CACF"}
1
+ {"version":3,"file":"errors.cjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all ward errors. Use `instanceof WardError` to catch any ward-originated error. */\nexport class WardError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is WardError {\n return err instanceof WardError;\n }\n}\n\n/** Thrown when a rule definition or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/**\n * Thrown when a `when` predicate in a ward rule throws an exception.\n *\n * Catch this to distinguish predicate failures from other errors:\n * ```ts\n * try {\n * ward.explain(principal, resource, action, data);\n * } catch (e) {\n * if (e instanceof WardPredicateError) {\n * console.error(`Predicate in Rule[${e.ruleIndex}] threw`, e.cause);\n * }\n * }\n * ```\n */\nexport class WardPredicateError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n\n super(`Rule[${ruleIndex}] threw: ${msg}`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n"],"mappings":"AACA,IAAa,EAAb,MAAa,UAAkB,KAAM,CACnC,YAAY,EAAiB,EAAqB,CAChD,MAAM,EAAS,CAAI,EACnB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CAEA,OAAO,GAAG,EAAgC,CACxC,OAAO,aAAe,CACxB,CACF,EAGa,EAAb,cAAqC,CAAU,CAAC,EAgBnC,EAAb,cAAwC,CAAU,CAChD,UAEA,YAAY,EAAmB,EAAgB,CAC7C,IAAM,EAAM,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EAEjE,MAAM,QAAQ,EAAU,WAAW,IAAO,CAAE,OAAM,CAAC,EACnD,KAAK,UAAY,CACnB,CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all ward errors. Use `instanceof WardError` to catch any ward-originated error. */\nexport class WardError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is WardError {\n return err instanceof WardError;\n }\n}\n\n/** Thrown when a rule definition or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/**\n * Thrown when a `when` predicate in a ward rule throws an exception.\n *\n * Catch this to distinguish predicate failures from other errors:\n * ```ts\n * try {\n * ward.explain(principal, resource, action, data);\n * } catch (e) {\n * if (e instanceof WardPredicateError) {\n * console.error(`Predicate in Rule[${e.ruleIndex}] threw`, e.cause);\n * }\n * }\n * ```\n */\nexport class WardPredicateError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n\n super(`Rule[${ruleIndex}] threw: ${msg}`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n"],"mappings":";AACA,IAAa,IAAb,MAAa,UAAkB,MAAM;CACnC,YAAY,GAAiB,GAAqB;EAGhD,AAFA,MAAM,GAAS,CAAI,GACnB,KAAK,OAAO,IAAI,OAAO,MACvB,OAAO,eAAe,MAAM,IAAI,OAAO,SAAS;CAClD;CAEA,OAAO,GAAG,GAAgC;EACxC,OAAO,aAAe;CACxB;AACF,GAGa,IAAb,cAAqC,EAAU,CAAC,GAgBnC,IAAb,cAAwC,EAAU;CAChD;CAEA,YAAY,GAAmB,GAAgB;EAC7C,IAAM,IAAM,aAAiB,QAAQ,EAAM,UAAU,OAAO,CAAK;EAGjE,AADA,MAAM,QAAQ,EAAU,WAAW,KAAO,EAAE,SAAM,CAAC,GACnD,KAAK,YAAY;CACnB;AACF"}
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Base class for all ward errors. Use `instanceof WardError` to catch any ward-originated error. */\nexport class WardError extends Error {\n constructor(message: string, opts?: ErrorOptions) {\n super(message, opts);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n static is(err: unknown): err is WardError {\n return err instanceof WardError;\n }\n}\n\n/** Thrown when a rule definition or principal is malformed. */\nexport class WardConfigError extends WardError {}\n\n/**\n * Thrown when a `when` predicate in a ward rule throws an exception.\n *\n * Catch this to distinguish predicate failures from other errors:\n * ```ts\n * try {\n * ward.explain(principal, resource, action, data);\n * } catch (e) {\n * if (e instanceof WardPredicateError) {\n * console.error(`Predicate in Rule[${e.ruleIndex}] threw`, e.cause);\n * }\n * }\n * ```\n */\nexport class WardPredicateError extends WardError {\n readonly ruleIndex: number;\n\n constructor(ruleIndex: number, cause: unknown) {\n const msg = cause instanceof Error ? cause.message : String(cause);\n\n super(`Rule[${ruleIndex}] threw: ${msg}`, { cause });\n this.ruleIndex = ruleIndex;\n }\n}\n"],"mappings":";AACA,IAAa,IAAb,MAAa,UAAkB,MAAM;CACnC,YAAY,GAAiB,GAAqB;EAGhD,AAFA,MAAM,GAAS,CAAI,GACnB,KAAK,OAAO,WAAW,MACvB,OAAO,eAAe,MAAM,WAAW,SAAS;CAClD;CAEA,OAAO,GAAG,GAAgC;EACxC,OAAO,aAAe;CACxB;AACF,GAGa,IAAb,cAAqC,EAAU,CAAC,GAgBnC,IAAb,cAAwC,EAAU;CAChD;CAEA,YAAY,GAAmB,GAAgB;EAC7C,IAAM,IAAM,aAAiB,QAAQ,EAAM,UAAU,OAAO,CAAK;EAGjE,AADA,MAAM,QAAQ,EAAU,WAAW,KAAO,EAAE,SAAM,CAAC,GACnD,KAAK,YAAY;CACnB;AACF"}
package/dist/factory.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("./errors.cjs"),t=require("./_compile.cjs"),n=require("./_match.cjs"),r=require("./_conflict.cjs");function i(e,t,r,i,a){let o=new Set,s=[];for(let c of i)o.has(c)||(o.add(c),n.pickWinner(e,t,r,c,a)?.rule.effect===`allow`&&s.push(c));return s}function a(e,t,r,i){let a=i===void 0,o=[];for(let s of e)n.matchesRule(s,t,r,void 0,i,a)&&o.push(s.rule);return o}function o(o=[],s={}){let{logger:c,maxConflicts:l=1/0}=s,u=o.map((e,n)=>t.compileEntry(e,n));function d(e,t,n,r,i){c&&c({...i,action:n,data:r,principal:e,resource:t})}function f(e,t,r,i){let a=n.toDecision(n.pickWinner(u,e,t,r,i));return d(e,t,r,i,a),a}function p(e){let{action:t,data:r,principal:i,resource:a}=e;return n.validatePrincipal(i),f(i,a,t,r)}function m(e,t){return t.map(t=>({...f(e,t.resource,t.action,t.data),action:t.action,resource:t.resource}))}function h(e,t){return t.length===0?[]:(n.validatePrincipal(e),m(e,t))}function g(e){let{data:t,knownActions:r,principal:a,resource:o}=e;return n.validatePrincipal(a),i(u,a,o,r,t)}function _(e){let{data:t,principal:r,resource:i}=e;return n.validatePrincipal(r),a(u,r,i,t)}function v(e){let{action:t,data:r,principal:i,resource:a}=e;n.validatePrincipal(i);let o=[];for(let e of u)n.matchesRule(e,i,a,t,r)&&o.push(e);let s;for(let e of o)(!s||n.isOverriddenBy(s,e))&&(s=e);let c=n.toDecision(s);return{candidates:o.map(e=>({index:e.index,priority:e.priority,rule:e.rule,score:e.score,won:e===s})),decision:c}}function y(e){n.assertUserPrincipal(e);let t={attributes:e.attributes?structuredClone(e.attributes):void 0,id:e.id,roles:[...e.roles]};return{allowedActions:e=>i(u,t,e.resource,e.knownActions,e.data),checkAll:e=>e.length===0?[]:m(t,e),explain:e=>f(t,e.resource,e.action,e.data),rulesInScope:e=>a(u,t,e.resource,e.data),trace:e=>v({action:e.action,data:e.data,principal:t,resource:e.resource})}}let b;function x(){return b??=Object.freeze(r.computeConflicts(u,l))}if(s.strict||s.onConflict){let t=x();if(t.length>0&&(s.onConflict&&t.forEach(s.onConflict),s.strict)){let n=t.map(e=>e.kind===`duplicate`?`Rule[${e.indexB}] ${e.kind} of Rule[${e.indexA}]`:`Rule[${e.shadowedIndex}] ${e.kind} by Rule[${e.shadowingIndex}]`).join(`; `);throw new e.WardConfigError(`${t.length} rule conflict(s) detected: ${n}`)}}return{allowedActions:g,checkAll:h,detectConflicts:x,explain:p,forUser:y,rulesInScope:_,trace:v}}exports.createWard=o;
1
+ const e=require("./errors.cjs"),t=require("./_compile.cjs"),n=require("./_match.cjs"),r=require("./_conflict.cjs");var i=require("./_dev.cjs").warnAnonymousPredicates;function a(e,t,r,i,a){let o=new Set,s=[];for(let c of i)o.has(c)||(o.add(c),n.pickWinner(e,t,r,c,a)?.rule.effect===`allow`&&s.push(c));return s}function o(e,t,r,i){let a=i===void 0,o=[];for(let s of e)n.matchesRule(s,t,r,void 0,i,a)&&o.push(s.rule);return o}function s(s=[],c={}){if(c.logger!==void 0&&typeof c.logger!=`function`)throw new e.WardConfigError(`logger must be a function.`);if(c.maxConflicts!==void 0&&(!Number.isFinite(c.maxConflicts)||c.maxConflicts<0))throw new e.WardConfigError(`maxConflicts must be a finite non-negative number.`);if(c.onConflict!==void 0&&typeof c.onConflict!=`function`)throw new e.WardConfigError(`onConflict must be a function.`);let{logger:l,maxConflicts:u=1/0}=c,d=s.map((e,n)=>t.compileEntry(e,n));i(d);function f(e,t,n,r,i){l&&l({...i,action:n,data:r,principal:e,resource:t})}function p(e,t,r,i){let a=n.pickWinner(d,e,t,r,i),o=n.toDecision(a);return f(e,t,r,i,o),o}function m(e){let{action:t,data:r,principal:i,resource:a}=e;return n.validatePrincipal(i),p(i,a,t,r)}function h(e,t){return t.map(t=>({...p(e,t.resource,t.action,t.data),action:t.action,resource:t.resource}))}function g(e,t){return t.length===0?[]:(n.validatePrincipal(e),h(e,t))}function _(e){let{data:t,knownActions:r,principal:i,resource:o}=e;return n.validatePrincipal(i),a(d,i,o,r,t)}function v(e){let{data:t,principal:r,resource:i}=e;return n.validatePrincipal(r),o(d,r,i,t)}function y(e){let{action:t,data:r,principal:i,resource:a}=e;n.validatePrincipal(i);let o=[];for(let e of d)n.matchesRule(e,i,a,t,r)&&o.push(e);let s;for(let e of o)(!s||n.isOverriddenBy(s,e))&&(s=e);let c=n.toDecision(s);return{candidates:o.map(e=>({index:e.index,priority:e.priority,rule:e.rule,score:e.score,won:e===s})),decision:c}}function b(e){n.assertUserPrincipal(e);let t={attributes:e.attributes?structuredClone(e.attributes):void 0,id:e.id,roles:[...e.roles]};return{allowedActions:e=>a(d,t,e.resource,e.knownActions,e.data),checkAll:e=>e.length===0?[]:h(t,e),explain:e=>p(t,e.resource,e.action,e.data),rulesInScope:e=>o(d,t,e.resource,e.data),trace:e=>y({action:e.action,data:e.data,principal:t,resource:e.resource})}}let x;function S(){return x??=Object.freeze(r.computeConflicts(d,u))}if(c.strict||c.onConflict){let t=S();if(t.length>0&&(c.onConflict&&t.forEach(c.onConflict),c.strict)){let n=t.map(e=>e.kind===`duplicate`?`Rule[${e.indexB}] ${e.kind} of Rule[${e.indexA}]`:`Rule[${e.shadowedIndex}] ${e.kind} by Rule[${e.shadowingIndex}]`).join(`; `);throw new e.WardConfigError(`${t.length} rule conflict(s) detected: ${n}`)}}return{allowedActions:_,checkAll:g,detectConflicts:S,explain:m,forUser:b,rulesInScope:v,trace:y}}exports.createWard=s;
2
2
  //# sourceMappingURL=factory.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"factory.cjs","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type {\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n BoundWard,\n Principal,\n UserPrincipal,\n WardAllowedActionsInput,\n Ward,\n WardCheck,\n WardConflict,\n WardDecisionInput,\n WardDecision,\n WardDecisionResult,\n WardOptions,\n WardRulesInScopeInput,\n WardRule,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\n\n// ---------------------------------------------------------------------------\n// Shared loop cores (validation-free; used by both public API and forUser)\n// ---------------------------------------------------------------------------\n\nfunction coreAllowedActions<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data: TData | undefined,\n): TAction[] {\n const seen = new Set<TAction>();\n const result: TAction[] = [];\n\n for (const action of knownActions) {\n if (seen.has(action)) continue;\n\n seen.add(action);\n\n const winner = pickWinner(entries, principal, resource, action, data);\n\n if (winner?.rule.effect === 'allow') result.push(action);\n }\n\n return result;\n}\n\nfunction coreRulesInScope<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n data: TData | undefined,\n): CompiledEntry<TAction, TData>['rule'][] {\n const skipPredicate = data === undefined;\n const result: CompiledEntry<TAction, TData>['rule'][] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n const { logger, maxConflicts = Infinity } = options;\n const entries = rules.map((rule, i) => compileEntry(rule, i));\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function fireLogger(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n decision: WardDecision<TAction, TData>,\n ): void {\n if (!logger) return;\n\n logger({ ...decision, action, data, principal, resource } as Parameters<typeof logger>[0]);\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n fireLogger(principal, resource, action, data, decision);\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n // Request objects avoid call-site ambiguity between resource/action/data and\n // make later API growth additive instead of positional-breaking.\n\n function explain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[] {\n const { data, knownActions, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(input: WardRulesInScopeInput<TData>): ReadonlyArray<Readonly<WardRule<TAction, TData>>> {\n const { data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (input: BoundWardAllowedActionsInput<TAction, TData>) =>\n coreAllowedActions(entries, snap, input.resource, input.knownActions, input.data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (input: BoundWardDecisionInput<TAction, TData>) =>\n evaluateAndLog(snap, input.resource, input.action, input.data),\n rulesInScope: (input: BoundWardRulesInScopeInput<TData>) =>\n coreRulesInScope(entries, snap, input.resource, input.data),\n trace: (input: BoundWardDecisionInput<TAction, TData>) =>\n trace({ action: input.action, data: input.data, principal: snap, resource: input.resource }),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, trace };\n}\n"],"mappings":"mHA+BA,SAAS,EACP,EACA,EACA,EACA,EACA,EACW,CACX,IAAM,EAAO,IAAI,IACX,EAAoB,CAAC,EAE3B,IAAK,IAAM,KAAU,EACf,EAAK,IAAI,CAAM,IAEnB,EAAK,IAAI,CAAM,EAEA,EAAA,WAAW,EAAS,EAAW,EAAU,EAAQ,CAE5D,CAAA,EAAQ,KAAK,SAAW,SAAS,EAAO,KAAK,CAAM,GAGzD,OAAO,CACT,CAEA,SAAS,EACP,EACA,EACA,EACA,EACyC,CACzC,IAAM,EAAgB,IAAS,IAAA,GACzB,EAAkD,CAAC,EAEzD,IAAK,IAAM,KAAS,EACb,EAAA,YAAY,EAAO,EAAW,EAAU,IAAA,GAAW,EAAM,CAAa,GAE3E,EAAO,KAAK,EAAM,IAAI,EAGxB,OAAO,CACT,CAiBA,SAAgB,EACd,EAA6C,CAAC,EAC9C,EAAuC,CAAC,EAClB,CACtB,GAAM,CAAE,SAAQ,eAAe,KAAa,EACtC,EAAU,EAAM,KAAK,EAAM,IAAM,EAAA,aAAa,EAAM,CAAC,CAAC,EAM5D,SAAS,EACP,EACA,EACA,EACA,EACA,EACM,CACD,GAEL,EAAO,CAAE,GAAG,EAAU,SAAQ,OAAM,YAAW,UAAS,CAAiC,CAC3F,CAEA,SAAS,EACP,EACA,EACA,EACA,EAC8B,CAE9B,IAAM,EAAW,EAAA,WADF,EAAA,WAAW,EAAS,EAAW,EAAU,EAAQ,CACpC,CAAM,EAIlC,OAFA,EAAW,EAAW,EAAU,EAAQ,EAAM,CAAQ,EAE/C,CACT,CAQA,SAAS,EAAQ,EAAwE,CACvF,GAAM,CAAE,SAAQ,OAAM,YAAW,YAAa,EAI9C,OAFA,EAAA,kBAAkB,CAAS,EAEpB,EAAe,EAAW,EAAU,EAAQ,CAAI,CACzD,CAEA,SAAS,EACP,EACA,EACsC,CACtC,OAAO,EAAO,IAAK,IAAW,CAC5B,GAAG,EAAe,EAAW,EAAM,SAAU,EAAM,OAAQ,EAAM,IAAI,EACrE,OAAQ,EAAM,OACd,SAAU,EAAM,QAClB,EAAE,CACJ,CAEA,SAAS,EACP,EACA,EACsC,CAKtC,OAJI,EAAO,SAAW,EAAU,CAAC,GAEjC,EAAA,kBAAkB,CAAS,EAEpB,EAAY,EAAW,CAAM,EACtC,CAEA,SAAS,EAAe,EAA2D,CACjF,GAAM,CAAE,OAAM,eAAc,YAAW,YAAa,EAIpD,OAFA,EAAA,kBAAkB,CAAS,EAEpB,EAAmB,EAAS,EAAW,EAAU,EAAc,CAAI,CAC5E,CAEA,SAAS,EAAa,EAAwF,CAC5G,GAAM,CAAE,OAAM,YAAW,YAAa,EAItC,OAFA,EAAA,kBAAkB,CAAS,EAEpB,EAAiB,EAAS,EAAW,EAAU,CAAI,CAC5D,CAEA,SAAS,EAAM,EAAqE,CAClF,GAAM,CAAE,SAAQ,OAAM,YAAW,YAAa,EAE9C,EAAA,kBAAkB,CAAS,EAE3B,IAAM,EAA4C,CAAC,EAEnD,IAAK,IAAM,KAAS,EACd,EAAA,YAAY,EAAO,EAAW,EAAU,EAAQ,CAAI,GACtD,EAAS,KAAK,CAAK,EAIvB,IAAI,EAEJ,IAAK,IAAM,KAAS,GACd,CAAC,GAAU,EAAA,eAAe,EAAQ,CAAK,KAAG,EAAS,GAGzD,IAAM,EAAW,EAAA,WAAW,CAAM,EAUlC,MAAO,CAAE,WARgD,EAAS,IAAK,IAAW,CAChF,MAAO,EAAM,MACb,SAAU,EAAM,SAChB,KAAM,EAAM,KACZ,MAAO,EAAM,MACb,IAAK,IAAU,CACjB,EAES,EAAY,UAAS,CAChC,CAEA,SAAS,EAAQ,EAAqD,CACpE,EAAA,oBAAoB,CAAS,EAE7B,IAAM,EAAsB,CAC1B,WAAY,EAAU,WAAa,gBAAgB,EAAU,UAAU,EAAI,IAAA,GAC3E,GAAI,EAAU,GACd,MAAO,CAAC,GAAG,EAAU,KAAK,CAC5B,EAEA,MAAO,CACL,eAAiB,GACf,EAAmB,EAAS,EAAM,EAAM,SAAU,EAAM,aAAc,EAAM,IAAI,EAClF,SAAW,GAAY,EAAO,SAAW,EAAI,CAAC,EAAI,EAAY,EAAM,CAAM,EAC1E,QAAU,GACR,EAAe,EAAM,EAAM,SAAU,EAAM,OAAQ,EAAM,IAAI,EAC/D,aAAe,GACb,EAAiB,EAAS,EAAM,EAAM,SAAU,EAAM,IAAI,EAC5D,MAAQ,GACN,EAAM,CAAE,OAAQ,EAAM,OAAQ,KAAM,EAAM,KAAM,UAAW,EAAM,SAAU,EAAM,QAAS,CAAC,CAC/F,CACF,CAMA,IAAI,EAEJ,SAAS,GAA2D,CAClE,MAAQ,KAAmB,OAAO,OAAO,EAAA,iBAAiB,EAAS,CAAY,CAAC,CAClF,CAEA,GAAI,EAAQ,QAAU,EAAQ,WAAY,CACxC,IAAM,EAAY,EAAgB,EAElC,GAAI,EAAU,OAAS,IACjB,EAAQ,YAAY,EAAU,QAAQ,EAAQ,UAAU,EAExD,EAAQ,QAAQ,CAClB,IAAM,EAAU,EACb,IAAK,GACJ,EAAE,OAAS,YACP,QAAQ,EAAE,OAAO,IAAI,EAAE,KAAK,WAAW,EAAE,OAAO,GAChD,QAAQ,EAAE,cAAc,IAAI,EAAE,KAAK,WAAW,EAAE,eAAe,EACrE,CAAC,CACA,KAAK,IAAI,EAEZ,MAAM,IAAI,EAAA,gBAAgB,GAAG,EAAU,OAAO,8BAA8B,GAAS,CACvF,CAEJ,CAEA,MAAO,CAAE,iBAAgB,WAAU,kBAAiB,UAAS,UAAS,eAAc,OAAM,CAC5F"}
1
+ {"version":3,"file":"factory.cjs","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type {\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n BoundWard,\n Principal,\n UserPrincipal,\n WardAllowedActionsInput,\n Ward,\n WardCheck,\n WardConflict,\n WardDecisionInput,\n WardDecision,\n WardDecisionResult,\n WardOptions,\n WardRulesInScopeInput,\n WardRule,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { warnAnonymousPredicates } from './_dev';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\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): CompiledEntry<TAction, TData>['rule'][] {\n const skipPredicate = data === undefined;\n const result: CompiledEntry<TAction, TData>['rule'][] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n if (options.logger !== undefined && typeof options.logger !== 'function') {\n throw new WardConfigError('logger must be a function.');\n }\n\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 { logger, maxConflicts = Infinity } = options;\n const entries = rules.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 fireLogger(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n decision: WardDecision<TAction, TData>,\n ): void {\n if (!logger) return;\n\n logger({ ...decision, action, data, principal, resource } as Parameters<typeof logger>[0]);\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n fireLogger(principal, resource, action, data, decision);\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n // Request objects avoid call-site ambiguity between resource/action/data and\n // make later API growth additive instead of positional-breaking.\n\n function explain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[] {\n const { data, knownActions, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(input: WardRulesInScopeInput<TData>): ReadonlyArray<Readonly<WardRule<TAction, TData>>> {\n const { data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (input: BoundWardAllowedActionsInput<TAction, TData>) =>\n coreAllowedActions(entries, snap, input.resource, input.knownActions, input.data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (input: BoundWardDecisionInput<TAction, TData>) =>\n evaluateAndLog(snap, input.resource, input.action, input.data),\n rulesInScope: (input: BoundWardRulesInScopeInput<TData>) =>\n coreRulesInScope(entries, snap, input.resource, input.data),\n trace: (input: BoundWardDecisionInput<TAction, TData>) =>\n trace({ action: input.action, data: input.data, principal: snap, resource: input.resource }),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, trace };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAQV,IAAI,EAMJ,WAAW,EAEX,QAAQ,EAGT,MAAM,SAAS,CAAC;AAwDjB;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACzE,KAAK,GAAE,SAAS,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAO,EAC/C,OAAO,GAAE,WAAW,CAAC,OAAO,EAAE,KAAK,CAAM,GACxC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CA2KtB"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAQV,IAAI,EAMJ,WAAW,EAEX,QAAQ,EAGT,MAAM,SAAS,CAAC;AA2DjB;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM,EAAE,KAAK,GAAG,OAAO,EACzE,KAAK,GAAE,SAAS,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAO,EAC/C,OAAO,GAAE,WAAW,CAAC,OAAO,EAAE,KAAK,CAAM,GACxC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CA4LtB"}
package/dist/factory.js CHANGED
@@ -2,21 +2,27 @@ import { WardConfigError as e } from "./errors.js";
2
2
  import { compileEntry as t } from "./_compile.js";
3
3
  import { assertUserPrincipal as n, isOverriddenBy as r, matchesRule as i, pickWinner as a, toDecision as o, validatePrincipal as s } from "./_match.js";
4
4
  import { computeConflicts as c } from "./_conflict.js";
5
+ import { warnAnonymousPredicates as l } from "./_dev.js";
5
6
  //#region src/factory.ts
6
- function l(e, t, n, r, i) {
7
+ var u = l;
8
+ function d(e, t, n, r, i) {
7
9
  let o = /* @__PURE__ */ new Set(), s = [];
8
10
  for (let c of r) o.has(c) || (o.add(c), a(e, t, n, c, i)?.rule.effect === "allow" && s.push(c));
9
11
  return s;
10
12
  }
11
- function u(e, t, n, r) {
13
+ function f(e, t, n, r) {
12
14
  let a = r === void 0, o = [];
13
15
  for (let s of e) i(s, t, n, void 0, r, a) && o.push(s.rule);
14
16
  return o;
15
17
  }
16
- function d(d = [], f = {}) {
17
- let { logger: p, maxConflicts: m = Infinity } = f, h = d.map((e, n) => t(e, n));
18
- function g(e, t, n, r, i) {
19
- p && p({
18
+ function p(l = [], p = {}) {
19
+ if (p.logger !== void 0 && typeof p.logger != "function") throw new e("logger must be a function.");
20
+ if (p.maxConflicts !== void 0 && (!Number.isFinite(p.maxConflicts) || p.maxConflicts < 0)) throw new e("maxConflicts must be a finite non-negative number.");
21
+ if (p.onConflict !== void 0 && typeof p.onConflict != "function") throw new e("onConflict must be a function.");
22
+ let { logger: m, maxConflicts: h = Infinity } = p, g = l.map((e, n) => t(e, n));
23
+ u(g);
24
+ function _(e, t, n, r, i) {
25
+ m && m({
20
26
  ...i,
21
27
  action: n,
22
28
  data: r,
@@ -24,37 +30,37 @@ function d(d = [], f = {}) {
24
30
  resource: t
25
31
  });
26
32
  }
27
- function _(e, t, n, r) {
28
- let i = o(a(h, e, t, n, r));
29
- return g(e, t, n, r, i), i;
33
+ function v(e, t, n, r) {
34
+ let i = a(g, e, t, n, r), s = o(i);
35
+ return _(e, t, n, r, s), s;
30
36
  }
31
- function v(e) {
37
+ function y(e) {
32
38
  let { action: t, data: n, principal: r, resource: i } = e;
33
- return s(r), _(r, i, t, n);
39
+ return s(r), v(r, i, t, n);
34
40
  }
35
- function y(e, t) {
41
+ function b(e, t) {
36
42
  return t.map((t) => ({
37
- ..._(e, t.resource, t.action, t.data),
43
+ ...v(e, t.resource, t.action, t.data),
38
44
  action: t.action,
39
45
  resource: t.resource
40
46
  }));
41
47
  }
42
- function b(e, t) {
43
- return t.length === 0 ? [] : (s(e), y(e, t));
48
+ function x(e, t) {
49
+ return t.length === 0 ? [] : (s(e), b(e, t));
44
50
  }
45
- function x(e) {
51
+ function S(e) {
46
52
  let { data: t, knownActions: n, principal: r, resource: i } = e;
47
- return s(r), l(h, r, i, n, t);
53
+ return s(r), d(g, r, i, n, t);
48
54
  }
49
- function S(e) {
55
+ function C(e) {
50
56
  let { data: t, principal: n, resource: r } = e;
51
- return s(n), u(h, n, r, t);
57
+ return s(n), f(g, n, r, t);
52
58
  }
53
- function C(e) {
59
+ function w(e) {
54
60
  let { action: t, data: n, principal: a, resource: c } = e;
55
61
  s(a);
56
62
  let l = [];
57
- for (let e of h) i(e, a, c, t, n) && l.push(e);
63
+ for (let e of g) i(e, a, c, t, n) && l.push(e);
58
64
  let u;
59
65
  for (let e of l) (!u || r(u, e)) && (u = e);
60
66
  let d = o(u);
@@ -69,7 +75,7 @@ function d(d = [], f = {}) {
69
75
  decision: d
70
76
  };
71
77
  }
72
- function w(e) {
78
+ function T(e) {
73
79
  n(e);
74
80
  let t = {
75
81
  attributes: e.attributes ? structuredClone(e.attributes) : void 0,
@@ -77,11 +83,11 @@ function d(d = [], f = {}) {
77
83
  roles: [...e.roles]
78
84
  };
79
85
  return {
80
- allowedActions: (e) => l(h, t, e.resource, e.knownActions, e.data),
81
- checkAll: (e) => e.length === 0 ? [] : y(t, e),
82
- explain: (e) => _(t, e.resource, e.action, e.data),
83
- rulesInScope: (e) => u(h, t, e.resource, e.data),
84
- trace: (e) => C({
86
+ allowedActions: (e) => d(g, t, e.resource, e.knownActions, e.data),
87
+ checkAll: (e) => e.length === 0 ? [] : b(t, e),
88
+ explain: (e) => v(t, e.resource, e.action, e.data),
89
+ rulesInScope: (e) => f(g, t, e.resource, e.data),
90
+ trace: (e) => w({
85
91
  action: e.action,
86
92
  data: e.data,
87
93
  principal: t,
@@ -89,28 +95,28 @@ function d(d = [], f = {}) {
89
95
  })
90
96
  };
91
97
  }
92
- let T;
93
- function E() {
94
- return T ??= Object.freeze(c(h, m));
98
+ let E;
99
+ function D() {
100
+ return E ??= Object.freeze(c(g, h));
95
101
  }
96
- if (f.strict || f.onConflict) {
97
- let t = E();
98
- if (t.length > 0 && (f.onConflict && t.forEach(f.onConflict), f.strict)) {
102
+ if (p.strict || p.onConflict) {
103
+ let t = D();
104
+ if (t.length > 0 && (p.onConflict && t.forEach(p.onConflict), p.strict)) {
99
105
  let n = t.map((e) => e.kind === "duplicate" ? `Rule[${e.indexB}] ${e.kind} of Rule[${e.indexA}]` : `Rule[${e.shadowedIndex}] ${e.kind} by Rule[${e.shadowingIndex}]`).join("; ");
100
106
  throw new e(`${t.length} rule conflict(s) detected: ${n}`);
101
107
  }
102
108
  }
103
109
  return {
104
- allowedActions: x,
105
- checkAll: b,
106
- detectConflicts: E,
107
- explain: v,
108
- forUser: w,
109
- rulesInScope: S,
110
- trace: C
110
+ allowedActions: S,
111
+ checkAll: x,
112
+ detectConflicts: D,
113
+ explain: y,
114
+ forUser: T,
115
+ rulesInScope: C,
116
+ trace: w
111
117
  };
112
118
  }
113
119
  //#endregion
114
- export { d as createWard };
120
+ export { p as createWard };
115
121
 
116
122
  //# sourceMappingURL=factory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"factory.js","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type {\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n BoundWard,\n Principal,\n UserPrincipal,\n WardAllowedActionsInput,\n Ward,\n WardCheck,\n WardConflict,\n WardDecisionInput,\n WardDecision,\n WardDecisionResult,\n WardOptions,\n WardRulesInScopeInput,\n WardRule,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\n\n// ---------------------------------------------------------------------------\n// Shared loop cores (validation-free; used by both public API and forUser)\n// ---------------------------------------------------------------------------\n\nfunction coreAllowedActions<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n knownActions: readonly TAction[],\n data: TData | undefined,\n): TAction[] {\n const seen = new Set<TAction>();\n const result: TAction[] = [];\n\n for (const action of knownActions) {\n if (seen.has(action)) continue;\n\n seen.add(action);\n\n const winner = pickWinner(entries, principal, resource, action, data);\n\n if (winner?.rule.effect === 'allow') result.push(action);\n }\n\n return result;\n}\n\nfunction coreRulesInScope<TAction extends string, TData>(\n entries: CompiledEntry<TAction, TData>[],\n principal: Principal,\n resource: string,\n data: TData | undefined,\n): CompiledEntry<TAction, TData>['rule'][] {\n const skipPredicate = data === undefined;\n const result: CompiledEntry<TAction, TData>['rule'][] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n const { logger, maxConflicts = Infinity } = options;\n const entries = rules.map((rule, i) => compileEntry(rule, i));\n\n // -------------------------------------------------------------------------\n // Core decision + logging\n // -------------------------------------------------------------------------\n\n function fireLogger(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n decision: WardDecision<TAction, TData>,\n ): void {\n if (!logger) return;\n\n logger({ ...decision, action, data, principal, resource } as Parameters<typeof logger>[0]);\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n fireLogger(principal, resource, action, data, decision);\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n // Request objects avoid call-site ambiguity between resource/action/data and\n // make later API growth additive instead of positional-breaking.\n\n function explain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[] {\n const { data, knownActions, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(input: WardRulesInScopeInput<TData>): ReadonlyArray<Readonly<WardRule<TAction, TData>>> {\n const { data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (input: BoundWardAllowedActionsInput<TAction, TData>) =>\n coreAllowedActions(entries, snap, input.resource, input.knownActions, input.data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (input: BoundWardDecisionInput<TAction, TData>) =>\n evaluateAndLog(snap, input.resource, input.action, input.data),\n rulesInScope: (input: BoundWardRulesInScopeInput<TData>) =>\n coreRulesInScope(entries, snap, input.resource, input.data),\n trace: (input: BoundWardDecisionInput<TAction, TData>) =>\n trace({ action: input.action, data: input.data, principal: snap, resource: input.resource }),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, trace };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"factory.js","names":[],"sources":["../src/factory.ts"],"sourcesContent":["import type { CompiledEntry } from './_compile';\nimport type {\n BoundWardAllowedActionsInput,\n BoundWardDecisionInput,\n BoundWardRulesInScopeInput,\n BoundWard,\n Principal,\n UserPrincipal,\n WardAllowedActionsInput,\n Ward,\n WardCheck,\n WardConflict,\n WardDecisionInput,\n WardDecision,\n WardDecisionResult,\n WardOptions,\n WardRulesInScopeInput,\n WardRule,\n WardTrace,\n WardTraceCandidate,\n} from './types';\n\nimport { compileEntry } from './_compile';\nimport { computeConflicts } from './_conflict';\nimport { warnAnonymousPredicates } from './_dev';\nimport { assertUserPrincipal, isOverriddenBy, matchesRule, pickWinner, toDecision, validatePrincipal } from './_match';\nimport { WardConfigError } from './errors';\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): CompiledEntry<TAction, TData>['rule'][] {\n const skipPredicate = data === undefined;\n const result: CompiledEntry<TAction, TData>['rule'][] = [];\n\n for (const entry of entries) {\n if (!matchesRule(entry, principal, resource, undefined, data, skipPredicate)) continue;\n\n result.push(entry.rule);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Creates an authorization ward from a set of rules.\n *\n * **Winner selection** when multiple rules match a request:\n * 1. Higher `priority` wins.\n * 2. On priority tie, higher specificity score wins (exact > namespace-wildcard > global-wildcard,\n * applied independently to role, resource, and action).\n * 3. On specificity tie, `deny` beats `allow` (denyBonus tiebreaker).\n * 4. On absolute tie (identical priority, specificity, and effect), the rule declared\n * **first in the input array** wins.\n */\nexport function createWard<TAction extends string = string, TData = unknown>(\n rules: readonly WardRule<TAction, TData>[] = [],\n options: WardOptions<TAction, TData> = {},\n): Ward<TAction, TData> {\n if (options.logger !== undefined && typeof options.logger !== 'function') {\n throw new WardConfigError('logger must be a function.');\n }\n\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 { logger, maxConflicts = Infinity } = options;\n const entries = rules.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 fireLogger(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n decision: WardDecision<TAction, TData>,\n ): void {\n if (!logger) return;\n\n logger({ ...decision, action, data, principal, resource } as Parameters<typeof logger>[0]);\n }\n\n function evaluateAndLog(\n principal: Principal,\n resource: string,\n action: TAction,\n data: TData | undefined,\n ): WardDecision<TAction, TData> {\n const winner = pickWinner(entries, principal, resource, action, data);\n const decision = toDecision(winner);\n\n fireLogger(principal, resource, action, data, decision);\n\n return decision;\n }\n\n // -------------------------------------------------------------------------\n // Public API\n // -------------------------------------------------------------------------\n // Request objects avoid call-site ambiguity between resource/action/data and\n // make later API growth additive instead of positional-breaking.\n\n function explain(input: WardDecisionInput<TAction, TData>): WardDecision<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return evaluateAndLog(principal, resource, action, data);\n }\n\n function runCheckAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n return checks.map((check) => ({\n ...evaluateAndLog(principal, check.resource, check.action, check.data),\n action: check.action,\n resource: check.resource,\n }));\n }\n\n function checkAll(\n principal: Principal,\n checks: readonly WardCheck<TAction, TData>[],\n ): WardDecisionResult<TAction, TData>[] {\n if (checks.length === 0) return [];\n\n validatePrincipal(principal);\n\n return runCheckAll(principal, checks);\n }\n\n function allowedActions(input: WardAllowedActionsInput<TAction, TData>): TAction[] {\n const { data, knownActions, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreAllowedActions(entries, principal, resource, knownActions, data);\n }\n\n function rulesInScope(input: WardRulesInScopeInput<TData>): ReadonlyArray<Readonly<WardRule<TAction, TData>>> {\n const { data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n return coreRulesInScope(entries, principal, resource, data);\n }\n\n function trace(input: WardDecisionInput<TAction, TData>): WardTrace<TAction, TData> {\n const { action, data, principal, resource } = input;\n\n validatePrincipal(principal);\n\n const matching: CompiledEntry<TAction, TData>[] = [];\n\n for (const entry of entries) {\n if (matchesRule(entry, principal, resource, action, data)) {\n matching.push(entry);\n }\n }\n\n let winner: CompiledEntry<TAction, TData> | undefined;\n\n for (const entry of matching) {\n if (!winner || isOverriddenBy(winner, entry)) winner = entry;\n }\n\n const decision = toDecision(winner);\n\n const candidates: WardTraceCandidate<TAction, TData>[] = matching.map((entry) => ({\n index: entry.index,\n priority: entry.priority,\n rule: entry.rule,\n score: entry.score,\n won: entry === winner,\n }));\n\n return { candidates, decision };\n }\n\n function forUser(principal: UserPrincipal): BoundWard<TAction, TData> {\n assertUserPrincipal(principal);\n\n const snap: UserPrincipal = {\n attributes: principal.attributes ? structuredClone(principal.attributes) : undefined,\n id: principal.id,\n roles: [...principal.roles],\n };\n\n return {\n allowedActions: (input: BoundWardAllowedActionsInput<TAction, TData>) =>\n coreAllowedActions(entries, snap, input.resource, input.knownActions, input.data),\n checkAll: (checks) => (checks.length === 0 ? [] : runCheckAll(snap, checks)),\n explain: (input: BoundWardDecisionInput<TAction, TData>) =>\n evaluateAndLog(snap, input.resource, input.action, input.data),\n rulesInScope: (input: BoundWardRulesInScopeInput<TData>) =>\n coreRulesInScope(entries, snap, input.resource, input.data),\n trace: (input: BoundWardDecisionInput<TAction, TData>) =>\n trace({ action: input.action, data: input.data, principal: snap, resource: input.resource }),\n };\n }\n\n // -------------------------------------------------------------------------\n // Conflict detection (lazy, cached)\n // -------------------------------------------------------------------------\n\n let conflictsCache: readonly WardConflict<TAction, TData>[] | undefined;\n\n function detectConflicts(): readonly WardConflict<TAction, TData>[] {\n return (conflictsCache ??= Object.freeze(computeConflicts(entries, maxConflicts)));\n }\n\n if (options.strict || options.onConflict) {\n const conflicts = detectConflicts();\n\n if (conflicts.length > 0) {\n if (options.onConflict) conflicts.forEach(options.onConflict);\n\n if (options.strict) {\n const details = conflicts\n .map((c) =>\n c.kind === 'duplicate'\n ? `Rule[${c.indexB}] ${c.kind} of Rule[${c.indexA}]`\n : `Rule[${c.shadowedIndex}] ${c.kind} by Rule[${c.shadowingIndex}]`,\n )\n .join('; ');\n\n throw new WardConfigError(`${conflicts.length} rule conflict(s) detected: ${details}`);\n }\n }\n }\n\n return { allowedActions, checkAll, detectConflicts, explain, forUser, rulesInScope, trace };\n}\n"],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./constants.cjs"),t=require("./errors.cjs"),n=require("./resource.cjs"),r=require("./factory.cjs"),i=require("./builder.cjs"),a=require("./middleware.cjs");exports.ANONYMOUS=e.ANONYMOUS,exports.WILDCARD=e.WILDCARD,exports.WardConfigError=t.WardConfigError,exports.WardError=t.WardError,exports.WardPredicateError=t.WardPredicateError,exports.allow=i.allow,exports.createWard=r.createWard,exports.deny=i.deny,exports.guardRequest=a.guardRequest,exports.guardRequestWith=a.guardRequestWith,exports.matchesPattern=n.matchesPattern,exports.owns=i.owns,exports.patternCovers=n.patternCovers,exports.predicate=i.predicate,exports.ruleFor=i.ruleFor;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./constants.cjs"),t=require("./errors.cjs"),n=require("./resource.cjs"),r=require("./factory.cjs"),i=require("./builder.cjs");exports.ANONYMOUS=e.ANONYMOUS,exports.WILDCARD=e.WILDCARD,exports.WardConfigError=t.WardConfigError,exports.WardError=t.WardError,exports.WardPredicateError=t.WardPredicateError,exports.allow=i.allow,exports.createWard=r.createWard,exports.deny=i.deny,exports.matchesPattern=n.matchesPattern,exports.owns=i.owns,exports.patternCovers=n.patternCovers,exports.predicate=i.predicate,exports.ruleFor=i.ruleFor;
package/dist/index.d.ts CHANGED
@@ -2,8 +2,6 @@ export { ANONYMOUS, WILDCARD } from './constants';
2
2
  export { allow, deny, owns, predicate, ruleFor } from './builder';
3
3
  export { WardConfigError, WardError, WardPredicateError } from './errors';
4
4
  export { createWard } from './factory';
5
- export { guardRequest, guardRequestWith } from './middleware';
6
5
  export { matchesPattern, patternCovers } from './resource';
7
6
  export type { BoundWardAllowedActionsInput, BoundWardDecisionInput, BoundWardRulesInScopeInput, BoundWard, ConflictKind, Principal, RuleContext, UserPrincipal, WardAllowedActionsInput, Ward, WardCheck, WardConflict, WardDecisionInput, WardDecision, WardDecisionResult, WardLoggerContext, WardOptions, WardPredicate, WardRulesInScopeInput, WardRule, WardTrace, WardTraceCandidate, } from './types';
8
- export type { GuardRequestInput, GuardRequestWithInput, GuardResult, PrincipalExtractor, WardRequest, } from './middleware';
9
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3D,YAAY,EACV,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,EAC1B,SAAS,EACT,YAAY,EACZ,SAAS,EACT,WAAW,EACX,aAAa,EACb,uBAAuB,EACvB,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,qBAAqB,EACrB,QAAQ,EACR,SAAS,EACT,kBAAkB,GACnB,MAAM,SAAS,CAAC;AACjB,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,WAAW,EACX,kBAAkB,EAClB,WAAW,GACZ,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3D,YAAY,EACV,4BAA4B,EAC5B,sBAAsB,EACtB,0BAA0B,EAC1B,SAAS,EACT,YAAY,EACZ,SAAS,EACT,WAAW,EACX,aAAa,EACb,uBAAuB,EACvB,IAAI,EACJ,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,qBAAqB,EACrB,QAAQ,EACR,SAAS,EACT,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -3,5 +3,4 @@ import { WardConfigError as n, WardError as r, WardPredicateError as i } from ".
3
3
  import { matchesPattern as a, patternCovers as o } from "./resource.js";
4
4
  import { createWard as s } from "./factory.js";
5
5
  import { allow as c, deny as l, owns as u, predicate as d, ruleFor as f } from "./builder.js";
6
- import { guardRequest as p, guardRequestWith as m } from "./middleware.js";
7
- export { e as ANONYMOUS, t as WILDCARD, n as WardConfigError, r as WardError, i as WardPredicateError, c as allow, s as createWard, l as deny, p as guardRequest, m as guardRequestWith, a as matchesPattern, u as owns, o as patternCovers, d as predicate, f as ruleFor };
6
+ export { e as ANONYMOUS, t as WILDCARD, n as WardConfigError, r as WardError, i as WardPredicateError, c as allow, s as createWard, l as deny, a as matchesPattern, u as owns, o as patternCovers, d as predicate, f as ruleFor };