@o3co/auth.policy-verifier.builtins 0.2.0 → 0.3.1

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.
Files changed (81) hide show
  1. package/README.ja.md +104 -2
  2. package/README.md +102 -0
  3. package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts +2 -0
  4. package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts.map +1 -0
  5. package/dist/__tests__/rules/AttrLiteralCompare.test.mjs +253 -0
  6. package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts +2 -0
  7. package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts.map +1 -0
  8. package/dist/__tests__/rules/AttrLiteralEqual.test.mjs +138 -0
  9. package/dist/__tests__/rules/AttrLiteralIn.test.d.mts +2 -0
  10. package/dist/__tests__/rules/AttrLiteralIn.test.d.mts.map +1 -0
  11. package/dist/__tests__/rules/AttrLiteralIn.test.mjs +160 -0
  12. package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts +2 -0
  13. package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts.map +1 -0
  14. package/dist/__tests__/rules/AttrLiteralNotEqual.test.mjs +157 -0
  15. package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts +2 -0
  16. package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts.map +1 -0
  17. package/dist/__tests__/rules/AttrLiteralNotIn.test.mjs +168 -0
  18. package/dist/__tests__/rules/AttrMatchRule.test.mjs +8 -0
  19. package/dist/__tests__/rules/AttrPairCompare.test.d.mts +2 -0
  20. package/dist/__tests__/rules/AttrPairCompare.test.d.mts.map +1 -0
  21. package/dist/__tests__/rules/AttrPairCompare.test.mjs +319 -0
  22. package/dist/__tests__/rules/AttrPairEqual.test.d.mts +2 -0
  23. package/dist/__tests__/rules/AttrPairEqual.test.d.mts.map +1 -0
  24. package/dist/__tests__/rules/AttrPairEqual.test.mjs +155 -0
  25. package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts +2 -0
  26. package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts.map +1 -0
  27. package/dist/__tests__/rules/AttrPairNotEqual.test.mjs +155 -0
  28. package/dist/__tests__/rules/_sharedValidation.test.d.mts +2 -0
  29. package/dist/__tests__/rules/_sharedValidation.test.d.mts.map +1 -0
  30. package/dist/__tests__/rules/_sharedValidation.test.mjs +325 -0
  31. package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.d.mts.map +1 -0
  32. package/dist/__tests__/rules/{ResourceActionPermissionRuleCollector.test.mjs → collectors/ResourceActionPermissionRuleCollector.test.mjs} +1 -1
  33. package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.d.mts.map +1 -0
  34. package/dist/__tests__/rules/{ResourceActionScopeRuleCollector.test.mjs → collectors/ResourceActionScopeRuleCollector.test.mjs} +1 -1
  35. package/dist/index.d.mts +10 -2
  36. package/dist/index.d.mts.map +1 -1
  37. package/dist/index.mjs +10 -2
  38. package/dist/module.mjs +2 -2
  39. package/dist/rules/AttrLiteralCompare.d.mts +31 -0
  40. package/dist/rules/AttrLiteralCompare.d.mts.map +1 -0
  41. package/dist/rules/AttrLiteralCompare.mjs +36 -0
  42. package/dist/rules/AttrLiteralEqual.d.mts +37 -0
  43. package/dist/rules/AttrLiteralEqual.d.mts.map +1 -0
  44. package/dist/rules/AttrLiteralEqual.mjs +43 -0
  45. package/dist/rules/AttrLiteralIn.d.mts +38 -0
  46. package/dist/rules/AttrLiteralIn.d.mts.map +1 -0
  47. package/dist/rules/AttrLiteralIn.mjs +46 -0
  48. package/dist/rules/AttrLiteralNotEqual.d.mts +36 -0
  49. package/dist/rules/AttrLiteralNotEqual.d.mts.map +1 -0
  50. package/dist/rules/AttrLiteralNotEqual.mjs +42 -0
  51. package/dist/rules/AttrLiteralNotIn.d.mts +33 -0
  52. package/dist/rules/AttrLiteralNotIn.d.mts.map +1 -0
  53. package/dist/rules/AttrLiteralNotIn.mjs +41 -0
  54. package/dist/rules/AttrMatchRule.d.mts +7 -34
  55. package/dist/rules/AttrMatchRule.d.mts.map +1 -1
  56. package/dist/rules/AttrMatchRule.mjs +7 -32
  57. package/dist/rules/AttrPairCompare.d.mts +35 -0
  58. package/dist/rules/AttrPairCompare.d.mts.map +1 -0
  59. package/dist/rules/AttrPairCompare.mjs +43 -0
  60. package/dist/rules/AttrPairEqual.d.mts +35 -0
  61. package/dist/rules/AttrPairEqual.d.mts.map +1 -0
  62. package/dist/rules/AttrPairEqual.mjs +44 -0
  63. package/dist/rules/AttrPairNotEqual.d.mts +34 -0
  64. package/dist/rules/AttrPairNotEqual.d.mts.map +1 -0
  65. package/dist/rules/AttrPairNotEqual.mjs +43 -0
  66. package/dist/rules/_sharedValidation.d.mts +87 -0
  67. package/dist/rules/_sharedValidation.d.mts.map +1 -0
  68. package/dist/rules/_sharedValidation.mjs +189 -0
  69. package/dist/rules/collectors/ResourceActionPermissionRuleCollector.d.mts.map +1 -0
  70. package/dist/rules/{ResourceActionPermissionRuleCollector.mjs → collectors/ResourceActionPermissionRuleCollector.mjs} +1 -1
  71. package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts.map +1 -0
  72. package/dist/rules/{ResourceActionScopeRuleCollector.mjs → collectors/ResourceActionScopeRuleCollector.mjs} +1 -1
  73. package/package.json +5 -2
  74. package/dist/__tests__/rules/ResourceActionPermissionRuleCollector.test.d.mts.map +0 -1
  75. package/dist/__tests__/rules/ResourceActionScopeRuleCollector.test.d.mts.map +0 -1
  76. package/dist/rules/ResourceActionPermissionRuleCollector.d.mts.map +0 -1
  77. package/dist/rules/ResourceActionScopeRuleCollector.d.mts.map +0 -1
  78. /package/dist/__tests__/rules/{ResourceActionPermissionRuleCollector.test.d.mts → collectors/ResourceActionPermissionRuleCollector.test.d.mts} +0 -0
  79. /package/dist/__tests__/rules/{ResourceActionScopeRuleCollector.test.d.mts → collectors/ResourceActionScopeRuleCollector.test.d.mts} +0 -0
  80. /package/dist/rules/{ResourceActionPermissionRuleCollector.d.mts → collectors/ResourceActionPermissionRuleCollector.d.mts} +0 -0
  81. /package/dist/rules/{ResourceActionScopeRuleCollector.d.mts → collectors/ResourceActionScopeRuleCollector.d.mts} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttrPairCompare.d.mts","sourceRoot":"","sources":["../../src/rules/AttrPairCompare.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAEN,KAAK,SAAS,EAId,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,qBAAqB;IACrC,CAAC,EAAE,MAAM,CAAC;IACV,EAAE,EAAE,SAAS,CAAC;IACd,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,eAAgB,YAAW,IAAI;IAK/B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,2BAA2B;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEI,MAAM,EAAE,qBAAqB;IAU1D,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;CAOlC"}
@@ -0,0 +1,43 @@
1
+ import { applyCompare, requireAttrName, requireCompareOp, requireOptionalGroup, } from "./_sharedValidation.mjs";
2
+ /**
3
+ * Rule that passes when two named attributes are present, are both numbers,
4
+ * and satisfy the configured comparison operator. NaN on either side always
5
+ * returns false (JS native comparison semantics). Missing, null, or non-number
6
+ * attributes return false (safe-deny).
7
+ *
8
+ * Unlike AttrLiteralCompare, the right-hand side comes from the attributes map
9
+ * at evaluation time rather than from a static config value — so NaN is not
10
+ * rejected at construction time.
11
+ *
12
+ * ## Grouping and the default ruleType
13
+ *
14
+ * The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
15
+ * across groups. The default `ruleType` is derived from `a`, `op`, and `b`:
16
+ * `attr_pair_compare:{a}:{op}:{b}`
17
+ *
18
+ * Pass an explicit `group` string to override the default ruleType entirely.
19
+ */
20
+ export class AttrPairCompare {
21
+ config;
22
+ ruleType;
23
+ code = "attr_compare_violated";
24
+ message;
25
+ constructor(config) {
26
+ this.config = config;
27
+ requireAttrName("AttrPairCompare", "a", config.a);
28
+ requireAttrName("AttrPairCompare", "b", config.b);
29
+ requireCompareOp("AttrPairCompare", config.op);
30
+ const group = requireOptionalGroup("AttrPairCompare", config.group);
31
+ this.ruleType = group ?? `attr_pair_compare:${config.a}:${config.op}:${config.b}`;
32
+ this.message = `Attribute constraint not satisfied: ${config.a} must be ${config.op} ${config.b}.`;
33
+ }
34
+ verify(attrs) {
35
+ const a = attrs.get(this.config.a);
36
+ const b = attrs.get(this.config.b);
37
+ if (typeof a !== "number")
38
+ return false;
39
+ if (typeof b !== "number")
40
+ return false;
41
+ return applyCompare(this.config.op, a, b);
42
+ }
43
+ }
@@ -0,0 +1,35 @@
1
+ import type { Attributes, Rule } from "@o3co/auth.policy-verifier.core";
2
+ export interface AttrPairEqualConfig {
3
+ a: string;
4
+ b: string;
5
+ group?: string;
6
+ }
7
+ /**
8
+ * Rule that passes when two named attributes are present, are non-empty
9
+ * strings, and are strictly equal. No type coercion is performed: both
10
+ * attribute values must be non-empty strings.
11
+ *
12
+ * ## Backward compatibility
13
+ *
14
+ * AttrPairEqual deliberately retains the string-only semantic of the legacy
15
+ * AttrMatchRule. Do not widen verify() to accept numbers or booleans — the
16
+ * Phase 4 AttrMatchRule alias depends on this invariant.
17
+ *
18
+ * ## Grouping and the default ruleType
19
+ *
20
+ * The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
21
+ * across groups. The default `ruleType` is derived from `a` and `b` so that
22
+ * distinct pair requirements are AND-combined by default:
23
+ * `attr_pair_equal:{a}:{b}`
24
+ *
25
+ * Pass a shared `group` string to two instances to opt into OR semantics.
26
+ */
27
+ export declare class AttrPairEqual implements Rule {
28
+ private readonly config;
29
+ readonly ruleType: string;
30
+ readonly code = "attr_mismatch";
31
+ readonly message: string;
32
+ constructor(config: AttrPairEqualConfig);
33
+ verify(attrs: Attributes): boolean;
34
+ }
35
+ //# sourceMappingURL=AttrPairEqual.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttrPairEqual.d.mts","sourceRoot":"","sources":["../../src/rules/AttrPairEqual.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iCAAiC,CAAC;AAGxE,MAAM,WAAW,mBAAmB;IACnC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,aAAc,YAAW,IAAI;IAK7B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,mBAAmB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEI,MAAM,EAAE,mBAAmB;IASxD,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;CAOlC"}
@@ -0,0 +1,44 @@
1
+ import { requireAttrName, requireOptionalGroup } from "./_sharedValidation.mjs";
2
+ /**
3
+ * Rule that passes when two named attributes are present, are non-empty
4
+ * strings, and are strictly equal. No type coercion is performed: both
5
+ * attribute values must be non-empty strings.
6
+ *
7
+ * ## Backward compatibility
8
+ *
9
+ * AttrPairEqual deliberately retains the string-only semantic of the legacy
10
+ * AttrMatchRule. Do not widen verify() to accept numbers or booleans — the
11
+ * Phase 4 AttrMatchRule alias depends on this invariant.
12
+ *
13
+ * ## Grouping and the default ruleType
14
+ *
15
+ * The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
16
+ * across groups. The default `ruleType` is derived from `a` and `b` so that
17
+ * distinct pair requirements are AND-combined by default:
18
+ * `attr_pair_equal:{a}:{b}`
19
+ *
20
+ * Pass a shared `group` string to two instances to opt into OR semantics.
21
+ */
22
+ export class AttrPairEqual {
23
+ config;
24
+ ruleType;
25
+ code = "attr_mismatch";
26
+ message;
27
+ constructor(config) {
28
+ this.config = config;
29
+ requireAttrName("AttrPairEqual", "a", config.a);
30
+ requireAttrName("AttrPairEqual", "b", config.b);
31
+ const group = requireOptionalGroup("AttrPairEqual", config.group);
32
+ this.ruleType = group ?? `attr_pair_equal:${config.a}:${config.b}`;
33
+ this.message = `Attribute constraint not satisfied: ${config.a} must equal ${config.b}.`;
34
+ }
35
+ verify(attrs) {
36
+ const a = attrs.get(this.config.a);
37
+ const b = attrs.get(this.config.b);
38
+ if (typeof a !== "string" || a.length === 0)
39
+ return false;
40
+ if (typeof b !== "string" || b.length === 0)
41
+ return false;
42
+ return a === b;
43
+ }
44
+ }
@@ -0,0 +1,34 @@
1
+ import type { Attributes, Rule } from "@o3co/auth.policy-verifier.core";
2
+ export interface AttrPairNotEqualConfig {
3
+ a: string;
4
+ b: string;
5
+ group?: string;
6
+ }
7
+ /**
8
+ * Rule that passes when two named attributes are present, are non-empty
9
+ * strings, and are strictly NOT equal. No type coercion is performed: both
10
+ * attribute values must be non-empty strings. Missing, null, empty, or
11
+ * non-string attributes return false (safe-deny).
12
+ *
13
+ * ## Denial code
14
+ *
15
+ * The denial code is "attr_match": the rule failed because the pair matched
16
+ * when it was required to differ.
17
+ *
18
+ * ## Grouping and the default ruleType
19
+ *
20
+ * The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
21
+ * across groups. The default `ruleType` is derived from `a` and `b`:
22
+ * `attr_pair_not_equal:{a}:{b}`
23
+ *
24
+ * Pass a shared `group` string to two instances to opt into OR semantics.
25
+ */
26
+ export declare class AttrPairNotEqual implements Rule {
27
+ private readonly config;
28
+ readonly ruleType: string;
29
+ readonly code = "attr_match";
30
+ readonly message: string;
31
+ constructor(config: AttrPairNotEqualConfig);
32
+ verify(attrs: Attributes): boolean;
33
+ }
34
+ //# sourceMappingURL=AttrPairNotEqual.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttrPairNotEqual.d.mts","sourceRoot":"","sources":["../../src/rules/AttrPairNotEqual.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,iCAAiC,CAAC;AAGxE,MAAM,WAAW,sBAAsB;IACtC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,gBAAiB,YAAW,IAAI;IAKhC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,gBAAgB;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEI,MAAM,EAAE,sBAAsB;IAS3D,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO;CAOlC"}
@@ -0,0 +1,43 @@
1
+ import { requireAttrName, requireOptionalGroup } from "./_sharedValidation.mjs";
2
+ /**
3
+ * Rule that passes when two named attributes are present, are non-empty
4
+ * strings, and are strictly NOT equal. No type coercion is performed: both
5
+ * attribute values must be non-empty strings. Missing, null, empty, or
6
+ * non-string attributes return false (safe-deny).
7
+ *
8
+ * ## Denial code
9
+ *
10
+ * The denial code is "attr_match": the rule failed because the pair matched
11
+ * when it was required to differ.
12
+ *
13
+ * ## Grouping and the default ruleType
14
+ *
15
+ * The evaluator groups rules by `ruleType`, ORs within a group, and ANDs
16
+ * across groups. The default `ruleType` is derived from `a` and `b`:
17
+ * `attr_pair_not_equal:{a}:{b}`
18
+ *
19
+ * Pass a shared `group` string to two instances to opt into OR semantics.
20
+ */
21
+ export class AttrPairNotEqual {
22
+ config;
23
+ ruleType;
24
+ code = "attr_match";
25
+ message;
26
+ constructor(config) {
27
+ this.config = config;
28
+ requireAttrName("AttrPairNotEqual", "a", config.a);
29
+ requireAttrName("AttrPairNotEqual", "b", config.b);
30
+ const group = requireOptionalGroup("AttrPairNotEqual", config.group);
31
+ this.ruleType = group ?? `attr_pair_not_equal:${config.a}:${config.b}`;
32
+ this.message = `Attribute constraint not satisfied: ${config.a} must not equal ${config.b}.`;
33
+ }
34
+ verify(attrs) {
35
+ const a = attrs.get(this.config.a);
36
+ const b = attrs.get(this.config.b);
37
+ if (typeof a !== "string" || a.length === 0)
38
+ return false;
39
+ if (typeof b !== "string" || b.length === 0)
40
+ return false;
41
+ return a !== b;
42
+ }
43
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared validation helpers for attribute-based rule constructors.
3
+ */
4
+ export type LiteralValue = string | number | boolean;
5
+ export type CompareOp = "lt" | "le" | "gt" | "ge";
6
+ export declare const COMPARE_OPS: readonly CompareOp[];
7
+ /**
8
+ * Validates that a config field used as an attribute name is a non-empty string
9
+ * that does not contain the `:` separator character used in derived `ruleType`
10
+ * strings. Allowing `:` would let distinct configs collide on the same
11
+ * `ruleType` (e.g. `(a="x:y", b="z")` and `(a="x", b="y:z")` both produce
12
+ * `attr_pair_equal:x:y:z`), which the evaluator would silently OR together
13
+ * and weaken authorization.
14
+ * @param className - The rule class name, used in error messages.
15
+ * @param fieldName - The config field name ("a" or "b"), used in error messages.
16
+ * @param value - The raw value to validate.
17
+ * @returns The validated non-empty string.
18
+ * @throws {Error} If value is not a non-empty string, or contains `:`.
19
+ */
20
+ export declare function requireAttrName(className: string, fieldName: "a" | "b", value: unknown): string;
21
+ /**
22
+ * Validates that a config field used as a literal comparison value is a
23
+ * string, number, or boolean (i.e. a LiteralValue). NaN is rejected because
24
+ * its comparison semantics (`NaN !== NaN` is always true, `NaN === NaN` is
25
+ * always false) would silently invert Equal/NotEqual rules — a NotEqual(v=NaN)
26
+ * would always pass against any numeric attribute, weakening authorization.
27
+ * @param className - The rule class name, used in error messages.
28
+ * @param fieldName - The config field name ("v"), used in error messages.
29
+ * @param value - The raw value to validate.
30
+ * @returns The validated LiteralValue.
31
+ * @throws {Error} If value is null, undefined, of any other type, or NaN.
32
+ */
33
+ export declare function requireLiteralValue(className: string, fieldName: "v", value: unknown): LiteralValue;
34
+ /**
35
+ * Validates a numeric comparison value. Rejects NaN; accepts Infinity/-Infinity.
36
+ * @param className - The rule class name, used in error messages.
37
+ * @param fieldName - The config field name ("v"), used in error messages.
38
+ * @param value - The raw value to validate.
39
+ * @returns The validated number.
40
+ * @throws {Error} If value is not a number, or if value is NaN.
41
+ */
42
+ export declare function requireNumber(className: string, fieldName: "v", value: unknown): number;
43
+ /**
44
+ * Validates that a value is a non-empty, homogeneous array of LiteralValues
45
+ * (no null/undefined elements, no NaN elements, all elements the same typeof).
46
+ * @param className - The rule class name, used in error messages.
47
+ * @param value - The raw value to validate.
48
+ * @returns The validated LiteralValue array.
49
+ * @throws {Error} If not an array, empty, contains invalid elements, has mixed types, or contains NaN.
50
+ */
51
+ export declare function requireHomogeneousLiteralArray(className: string, value: unknown): LiteralValue[];
52
+ /**
53
+ * Validates that a value is one of the allowed CompareOp strings.
54
+ * @param className - The rule class name, used in error messages.
55
+ * @param value - The raw value to validate.
56
+ * @returns The validated CompareOp.
57
+ * @throws {Error} If value is not a string or not one of the allowed operators.
58
+ */
59
+ export declare function requireCompareOp(className: string, value: unknown): CompareOp;
60
+ /**
61
+ * Validates an optional group string. Returns undefined when value is undefined;
62
+ * throws when value is a non-string or an empty string; otherwise returns the
63
+ * non-empty string.
64
+ * @param className - The rule class name, used in error messages.
65
+ * @param value - The raw value to validate.
66
+ * @returns The validated non-empty string, or undefined.
67
+ * @throws {Error} If value is a non-string or an empty string.
68
+ */
69
+ export declare function requireOptionalGroup(className: string, value: unknown): string | undefined;
70
+ /**
71
+ * Computes a stable cache/dedup key for an array of LiteralValues.
72
+ * Format: `{type}:{count}:{hashPrefix}` — SHA-256 over a canonical serialization
73
+ * of `values`. The canonical form first deduplicates `values` (to mirror the
74
+ * Set-based semantics of AttrLiteralIn / AttrLiteralNotIn — duplicates do not
75
+ * change the rule's behavior, so they must not change its `ruleType`), then
76
+ * applies `JSON.stringify(String(v))` to each remaining element before sorting
77
+ * and joining with `,`. The per-element quoting prevents separator collisions
78
+ * across different arrays (e.g. `["a,b", "c"]` vs `["a", "b,c"]`).
79
+ * The `{count}` segment reflects the post-dedup element count.
80
+ * Caller must guarantee values is a non-empty homogeneous LiteralValue[].
81
+ */
82
+ export declare function computeValuesKey(values: LiteralValue[]): string;
83
+ /**
84
+ * Applies a numeric comparison operator between left and right.
85
+ */
86
+ export declare function applyCompare<T extends number>(op: CompareOp, left: T, right: T): boolean;
87
+ //# sourceMappingURL=_sharedValidation.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_sharedValidation.d.mts","sourceRoot":"","sources":["../../src/rules/_sharedValidation.mts"],"names":[],"mappings":"AAEA;;GAEG;AAEH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAarD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAElD,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAA6B,CAAC;AAE1E;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,GAAG,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAe/F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,GAAG,EACd,KAAK,EAAE,OAAO,GACZ,YAAY,CAad;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAQvF;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAkChG;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,CAU7E;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAe1F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,CAS/D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAWxF"}
@@ -0,0 +1,189 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Returns a human-readable type label for use in error messages.
4
+ * Distinguishes null and undefined from other types instead of returning
5
+ * "object" or "undefined" from typeof.
6
+ */
7
+ function describeType(value) {
8
+ if (value === null)
9
+ return "null";
10
+ if (value === undefined)
11
+ return "undefined";
12
+ return typeof value;
13
+ }
14
+ export const COMPARE_OPS = ["lt", "le", "gt", "ge"];
15
+ /**
16
+ * Validates that a config field used as an attribute name is a non-empty string
17
+ * that does not contain the `:` separator character used in derived `ruleType`
18
+ * strings. Allowing `:` would let distinct configs collide on the same
19
+ * `ruleType` (e.g. `(a="x:y", b="z")` and `(a="x", b="y:z")` both produce
20
+ * `attr_pair_equal:x:y:z`), which the evaluator would silently OR together
21
+ * and weaken authorization.
22
+ * @param className - The rule class name, used in error messages.
23
+ * @param fieldName - The config field name ("a" or "b"), used in error messages.
24
+ * @param value - The raw value to validate.
25
+ * @returns The validated non-empty string.
26
+ * @throws {Error} If value is not a non-empty string, or contains `:`.
27
+ */
28
+ export function requireAttrName(className, fieldName, value) {
29
+ if (typeof value !== "string") {
30
+ throw new Error(`${className}: '${fieldName}' must be a non-empty string, got ${describeType(value)}`);
31
+ }
32
+ if (value.length === 0) {
33
+ throw new Error(`${className}: '${fieldName}' must be a non-empty string, got empty string`);
34
+ }
35
+ if (value.includes(":")) {
36
+ throw new Error(`${className}: '${fieldName}' must not contain ':' (reserved as ruleType separator), got '${value}'`);
37
+ }
38
+ return value;
39
+ }
40
+ /**
41
+ * Validates that a config field used as a literal comparison value is a
42
+ * string, number, or boolean (i.e. a LiteralValue). NaN is rejected because
43
+ * its comparison semantics (`NaN !== NaN` is always true, `NaN === NaN` is
44
+ * always false) would silently invert Equal/NotEqual rules — a NotEqual(v=NaN)
45
+ * would always pass against any numeric attribute, weakening authorization.
46
+ * @param className - The rule class name, used in error messages.
47
+ * @param fieldName - The config field name ("v"), used in error messages.
48
+ * @param value - The raw value to validate.
49
+ * @returns The validated LiteralValue.
50
+ * @throws {Error} If value is null, undefined, of any other type, or NaN.
51
+ */
52
+ export function requireLiteralValue(className, fieldName, value) {
53
+ if (typeof value === "string" || typeof value === "boolean") {
54
+ return value;
55
+ }
56
+ if (typeof value === "number") {
57
+ if (Number.isNaN(value)) {
58
+ throw new Error(`${className}: '${fieldName}' must not be NaN`);
59
+ }
60
+ return value;
61
+ }
62
+ throw new Error(`${className}: '${fieldName}' must be string | number | boolean, got ${describeType(value)}`);
63
+ }
64
+ /**
65
+ * Validates a numeric comparison value. Rejects NaN; accepts Infinity/-Infinity.
66
+ * @param className - The rule class name, used in error messages.
67
+ * @param fieldName - The config field name ("v"), used in error messages.
68
+ * @param value - The raw value to validate.
69
+ * @returns The validated number.
70
+ * @throws {Error} If value is not a number, or if value is NaN.
71
+ */
72
+ export function requireNumber(className, fieldName, value) {
73
+ if (typeof value !== "number") {
74
+ throw new Error(`${className}: '${fieldName}' must be a number, got ${describeType(value)}`);
75
+ }
76
+ if (Number.isNaN(value)) {
77
+ throw new Error(`${className}: '${fieldName}' must not be NaN`);
78
+ }
79
+ return value;
80
+ }
81
+ /**
82
+ * Validates that a value is a non-empty, homogeneous array of LiteralValues
83
+ * (no null/undefined elements, no NaN elements, all elements the same typeof).
84
+ * @param className - The rule class name, used in error messages.
85
+ * @param value - The raw value to validate.
86
+ * @returns The validated LiteralValue array.
87
+ * @throws {Error} If not an array, empty, contains invalid elements, has mixed types, or contains NaN.
88
+ */
89
+ export function requireHomogeneousLiteralArray(className, value) {
90
+ if (!Array.isArray(value)) {
91
+ throw new Error(`${className}: 'values' must be a non-empty array of string | number | boolean, got ${describeType(value)}`);
92
+ }
93
+ if (value.length === 0) {
94
+ throw new Error(`${className}: 'values' must be a non-empty array`);
95
+ }
96
+ const seenTypes = new Set();
97
+ for (const element of value) {
98
+ if (element === null || element === undefined) {
99
+ throw new Error(`${className}: 'values' elements must be string | number | boolean, got ${describeType(element)}`);
100
+ }
101
+ const t = typeof element;
102
+ if (t !== "string" && t !== "number" && t !== "boolean") {
103
+ throw new Error(`${className}: 'values' elements must be string | number | boolean, got ${t}`);
104
+ }
105
+ if (t === "number" && Number.isNaN(element)) {
106
+ throw new Error(`${className}: 'values' must not contain NaN`);
107
+ }
108
+ seenTypes.add(t);
109
+ }
110
+ if (seenTypes.size > 1) {
111
+ const typeList = [...seenTypes].join(", ");
112
+ throw new Error(`${className}: 'values' must be a homogeneous array of string | number | boolean; got mixed types [${typeList}]`);
113
+ }
114
+ return value;
115
+ }
116
+ /**
117
+ * Validates that a value is one of the allowed CompareOp strings.
118
+ * @param className - The rule class name, used in error messages.
119
+ * @param value - The raw value to validate.
120
+ * @returns The validated CompareOp.
121
+ * @throws {Error} If value is not a string or not one of the allowed operators.
122
+ */
123
+ export function requireCompareOp(className, value) {
124
+ if (typeof value !== "string") {
125
+ throw new Error(`${className}: 'op' must be one of ${COMPARE_OPS.join(" | ")}, got ${describeType(value)}`);
126
+ }
127
+ if (!COMPARE_OPS.includes(value)) {
128
+ throw new Error(`${className}: 'op' must be one of ${COMPARE_OPS.join(" | ")}, got '${value}'`);
129
+ }
130
+ return value;
131
+ }
132
+ /**
133
+ * Validates an optional group string. Returns undefined when value is undefined;
134
+ * throws when value is a non-string or an empty string; otherwise returns the
135
+ * non-empty string.
136
+ * @param className - The rule class name, used in error messages.
137
+ * @param value - The raw value to validate.
138
+ * @returns The validated non-empty string, or undefined.
139
+ * @throws {Error} If value is a non-string or an empty string.
140
+ */
141
+ export function requireOptionalGroup(className, value) {
142
+ if (value === undefined) {
143
+ return undefined;
144
+ }
145
+ if (typeof value !== "string") {
146
+ throw new Error(`${className}: 'group' must be a non-empty string or undefined, got ${describeType(value)}`);
147
+ }
148
+ if (value.length === 0) {
149
+ throw new Error(`${className}: 'group' must be a non-empty string or undefined, got empty string`);
150
+ }
151
+ return value;
152
+ }
153
+ /**
154
+ * Computes a stable cache/dedup key for an array of LiteralValues.
155
+ * Format: `{type}:{count}:{hashPrefix}` — SHA-256 over a canonical serialization
156
+ * of `values`. The canonical form first deduplicates `values` (to mirror the
157
+ * Set-based semantics of AttrLiteralIn / AttrLiteralNotIn — duplicates do not
158
+ * change the rule's behavior, so they must not change its `ruleType`), then
159
+ * applies `JSON.stringify(String(v))` to each remaining element before sorting
160
+ * and joining with `,`. The per-element quoting prevents separator collisions
161
+ * across different arrays (e.g. `["a,b", "c"]` vs `["a", "b,c"]`).
162
+ * The `{count}` segment reflects the post-dedup element count.
163
+ * Caller must guarantee values is a non-empty homogeneous LiteralValue[].
164
+ */
165
+ export function computeValuesKey(values) {
166
+ const elementType = typeof values[0];
167
+ const unique = [...new Set(values)];
168
+ const joined = unique
169
+ .map((v) => JSON.stringify(String(v)))
170
+ .sort()
171
+ .join(",");
172
+ const hash = createHash("sha256").update(joined).digest("hex").slice(0, 8);
173
+ return `${elementType}:${unique.length}:${hash}`;
174
+ }
175
+ /**
176
+ * Applies a numeric comparison operator between left and right.
177
+ */
178
+ export function applyCompare(op, left, right) {
179
+ switch (op) {
180
+ case "lt":
181
+ return left < right;
182
+ case "le":
183
+ return left <= right;
184
+ case "gt":
185
+ return left > right;
186
+ case "ge":
187
+ return left >= right;
188
+ }
189
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResourceActionPermissionRuleCollector.d.mts","sourceRoot":"","sources":["../../../src/rules/collectors/ResourceActionPermissionRuleCollector.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAG7F,qBAAa,qCAAsC,YAAW,aAAa;IACpE,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAIzD"}
@@ -1,4 +1,4 @@
1
- import { HasPermission } from "./HasPermission.mjs";
1
+ import { HasPermission } from "../HasPermission.mjs";
2
2
  export class ResourceActionPermissionRuleCollector {
3
3
  async collect(context) {
4
4
  const permission = `${context.resource.raw}.perm:${context.action}`;
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResourceActionScopeRuleCollector.d.mts","sourceRoot":"","sources":["../../../src/rules/collectors/ResourceActionScopeRuleCollector.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAG7F;;;;;;;;GAQG;AACH,qBAAa,gCAAiC,YAAW,aAAa;IAC/D,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAOzD"}
@@ -1,4 +1,4 @@
1
- import { HasScope } from "./HasScope.mjs";
1
+ import { HasScope } from "../HasScope.mjs";
2
2
  /**
3
3
  * Generates a HasScope rule derived from the request action and resource type.
4
4
  *
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@o3co/auth.policy-verifier.builtins",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=22.0.0"
8
+ },
6
9
  "main": "./dist/index.mjs",
7
10
  "types": "./dist/index.d.mts",
8
11
  "exports": {
@@ -28,7 +31,7 @@
28
31
  }
29
32
  },
30
33
  "dependencies": {
31
- "@o3co/auth.policy-verifier.core": "0.2.0"
34
+ "@o3co/auth.policy-verifier.core": "0.3.1"
32
35
  },
33
36
  "devDependencies": {
34
37
  "@types/node": "^25.1.0",
@@ -1 +0,0 @@
1
- {"version":3,"file":"ResourceActionPermissionRuleCollector.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/ResourceActionPermissionRuleCollector.test.mts"],"names":[],"mappings":""}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ResourceActionScopeRuleCollector.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/ResourceActionScopeRuleCollector.test.mts"],"names":[],"mappings":""}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ResourceActionPermissionRuleCollector.d.mts","sourceRoot":"","sources":["../../src/rules/ResourceActionPermissionRuleCollector.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAG7F,qBAAa,qCAAsC,YAAW,aAAa;IACpE,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAIzD"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"ResourceActionScopeRuleCollector.d.mts","sourceRoot":"","sources":["../../src/rules/ResourceActionScopeRuleCollector.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAG7F;;;;;;;;GAQG;AACH,qBAAa,gCAAiC,YAAW,aAAa;IAC/D,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;CAOzD"}