@o3co/auth.policy-verifier.builtins 0.3.1 → 0.5.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.
Files changed (128) hide show
  1. package/README.ja.md +70 -14
  2. package/README.md +75 -15
  3. package/dist/collectors/PayloadScopeCollector.d.mts +5 -0
  4. package/dist/collectors/PayloadScopeCollector.d.mts.map +1 -1
  5. package/dist/collectors/PayloadScopeCollector.mjs +8 -1
  6. package/dist/collectors/PayloadSubjectIdCollector.d.mts +9 -0
  7. package/dist/collectors/PayloadSubjectIdCollector.d.mts.map +1 -1
  8. package/dist/collectors/PayloadSubjectIdCollector.mjs +16 -4
  9. package/dist/collectors/RequestContextAttributeCollector.d.mts +47 -0
  10. package/dist/collectors/RequestContextAttributeCollector.d.mts.map +1 -0
  11. package/dist/collectors/RequestContextAttributeCollector.mjs +100 -0
  12. package/dist/collectors/StaticPermissionCollector.d.mts +5 -0
  13. package/dist/collectors/StaticPermissionCollector.d.mts.map +1 -1
  14. package/dist/collectors/StaticPermissionCollector.mjs +7 -0
  15. package/dist/collectors/StaticRoleCollector.d.mts +5 -0
  16. package/dist/collectors/StaticRoleCollector.d.mts.map +1 -1
  17. package/dist/collectors/StaticRoleCollector.mjs +7 -0
  18. package/dist/index.d.mts +3 -2
  19. package/dist/index.d.mts.map +1 -1
  20. package/dist/index.mjs +4 -1
  21. package/dist/module.d.mts +5 -0
  22. package/dist/module.d.mts.map +1 -1
  23. package/dist/module.mjs +10 -1
  24. package/dist/resource/DotNotationResourceParser.d.mts +48 -0
  25. package/dist/resource/DotNotationResourceParser.d.mts.map +1 -1
  26. package/dist/resource/DotNotationResourceParser.mjs +94 -10
  27. package/dist/rules/AttrLiteralCompare.d.mts +2 -2
  28. package/dist/rules/AttrLiteralCompare.d.mts.map +1 -1
  29. package/dist/rules/AttrLiteralCompare.mjs +2 -0
  30. package/dist/rules/AttrLiteralEqual.d.mts +2 -2
  31. package/dist/rules/AttrLiteralEqual.d.mts.map +1 -1
  32. package/dist/rules/AttrLiteralEqual.mjs +2 -0
  33. package/dist/rules/AttrLiteralIn.d.mts +2 -2
  34. package/dist/rules/AttrLiteralIn.d.mts.map +1 -1
  35. package/dist/rules/AttrLiteralIn.mjs +2 -0
  36. package/dist/rules/AttrLiteralNotEqual.d.mts +2 -2
  37. package/dist/rules/AttrLiteralNotEqual.d.mts.map +1 -1
  38. package/dist/rules/AttrLiteralNotEqual.mjs +2 -0
  39. package/dist/rules/AttrLiteralNotIn.d.mts +2 -2
  40. package/dist/rules/AttrLiteralNotIn.d.mts.map +1 -1
  41. package/dist/rules/AttrLiteralNotIn.mjs +2 -0
  42. package/dist/rules/AttrMatchRule.d.mts.map +1 -1
  43. package/dist/rules/AttrMatchRule.mjs +2 -0
  44. package/dist/rules/AttrPairCompare.d.mts +2 -2
  45. package/dist/rules/AttrPairCompare.d.mts.map +1 -1
  46. package/dist/rules/AttrPairCompare.mjs +2 -0
  47. package/dist/rules/AttrPairEqual.d.mts +2 -2
  48. package/dist/rules/AttrPairEqual.d.mts.map +1 -1
  49. package/dist/rules/AttrPairEqual.mjs +2 -0
  50. package/dist/rules/AttrPairNotEqual.d.mts +2 -2
  51. package/dist/rules/AttrPairNotEqual.d.mts.map +1 -1
  52. package/dist/rules/AttrPairNotEqual.mjs +2 -0
  53. package/dist/rules/HasPermission.d.mts +36 -2
  54. package/dist/rules/HasPermission.d.mts.map +1 -1
  55. package/dist/rules/HasPermission.mjs +57 -6
  56. package/dist/rules/HasScope.d.mts +45 -4
  57. package/dist/rules/HasScope.d.mts.map +1 -1
  58. package/dist/rules/HasScope.mjs +46 -9
  59. package/dist/rules/_sharedValidation.d.mts +12 -9
  60. package/dist/rules/_sharedValidation.d.mts.map +1 -1
  61. package/dist/rules/_sharedValidation.mjs +46 -9
  62. package/dist/rules/collectors/ResourceActionPermissionRuleCollector.d.mts +6 -0
  63. package/dist/rules/collectors/ResourceActionPermissionRuleCollector.d.mts.map +1 -1
  64. package/dist/rules/collectors/ResourceActionPermissionRuleCollector.mjs +8 -0
  65. package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts +52 -4
  66. package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts.map +1 -1
  67. package/dist/rules/collectors/ResourceActionScopeRuleCollector.mjs +54 -6
  68. package/package.json +8 -5
  69. package/dist/__tests__/collectors/PayloadScopeCollector.test.d.mts +0 -2
  70. package/dist/__tests__/collectors/PayloadScopeCollector.test.d.mts.map +0 -1
  71. package/dist/__tests__/collectors/PayloadScopeCollector.test.mjs +0 -24
  72. package/dist/__tests__/collectors/PayloadSubjectIdCollector.test.d.mts +0 -2
  73. package/dist/__tests__/collectors/PayloadSubjectIdCollector.test.d.mts.map +0 -1
  74. package/dist/__tests__/collectors/PayloadSubjectIdCollector.test.mjs +0 -33
  75. package/dist/__tests__/collectors/StaticPermissionCollector.test.d.mts +0 -2
  76. package/dist/__tests__/collectors/StaticPermissionCollector.test.d.mts.map +0 -1
  77. package/dist/__tests__/collectors/StaticPermissionCollector.test.mjs +0 -22
  78. package/dist/__tests__/collectors/StaticRoleCollector.test.d.mts +0 -2
  79. package/dist/__tests__/collectors/StaticRoleCollector.test.d.mts.map +0 -1
  80. package/dist/__tests__/collectors/StaticRoleCollector.test.mjs +0 -24
  81. package/dist/__tests__/module.test.d.mts +0 -2
  82. package/dist/__tests__/module.test.d.mts.map +0 -1
  83. package/dist/__tests__/module.test.mjs +0 -71
  84. package/dist/__tests__/resource/DotNotationResourceParser.test.d.mts +0 -2
  85. package/dist/__tests__/resource/DotNotationResourceParser.test.d.mts.map +0 -1
  86. package/dist/__tests__/resource/DotNotationResourceParser.test.mjs +0 -34
  87. package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts +0 -2
  88. package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts.map +0 -1
  89. package/dist/__tests__/rules/AttrLiteralCompare.test.mjs +0 -253
  90. package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts +0 -2
  91. package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts.map +0 -1
  92. package/dist/__tests__/rules/AttrLiteralEqual.test.mjs +0 -138
  93. package/dist/__tests__/rules/AttrLiteralIn.test.d.mts +0 -2
  94. package/dist/__tests__/rules/AttrLiteralIn.test.d.mts.map +0 -1
  95. package/dist/__tests__/rules/AttrLiteralIn.test.mjs +0 -160
  96. package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts +0 -2
  97. package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts.map +0 -1
  98. package/dist/__tests__/rules/AttrLiteralNotEqual.test.mjs +0 -157
  99. package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts +0 -2
  100. package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts.map +0 -1
  101. package/dist/__tests__/rules/AttrLiteralNotIn.test.mjs +0 -168
  102. package/dist/__tests__/rules/AttrMatchRule.test.d.mts +0 -2
  103. package/dist/__tests__/rules/AttrMatchRule.test.d.mts.map +0 -1
  104. package/dist/__tests__/rules/AttrMatchRule.test.mjs +0 -79
  105. package/dist/__tests__/rules/AttrPairCompare.test.d.mts +0 -2
  106. package/dist/__tests__/rules/AttrPairCompare.test.d.mts.map +0 -1
  107. package/dist/__tests__/rules/AttrPairCompare.test.mjs +0 -319
  108. package/dist/__tests__/rules/AttrPairEqual.test.d.mts +0 -2
  109. package/dist/__tests__/rules/AttrPairEqual.test.d.mts.map +0 -1
  110. package/dist/__tests__/rules/AttrPairEqual.test.mjs +0 -155
  111. package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts +0 -2
  112. package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts.map +0 -1
  113. package/dist/__tests__/rules/AttrPairNotEqual.test.mjs +0 -155
  114. package/dist/__tests__/rules/HasPermission.test.d.mts +0 -2
  115. package/dist/__tests__/rules/HasPermission.test.d.mts.map +0 -1
  116. package/dist/__tests__/rules/HasPermission.test.mjs +0 -86
  117. package/dist/__tests__/rules/HasScope.test.d.mts +0 -2
  118. package/dist/__tests__/rules/HasScope.test.d.mts.map +0 -1
  119. package/dist/__tests__/rules/HasScope.test.mjs +0 -42
  120. package/dist/__tests__/rules/_sharedValidation.test.d.mts +0 -2
  121. package/dist/__tests__/rules/_sharedValidation.test.d.mts.map +0 -1
  122. package/dist/__tests__/rules/_sharedValidation.test.mjs +0 -325
  123. package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.d.mts +0 -2
  124. package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.d.mts.map +0 -1
  125. package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.mjs +0 -27
  126. package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.d.mts +0 -2
  127. package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.d.mts.map +0 -1
  128. package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.mjs +0 -48
@@ -1,86 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { HasPermission } from "#/rules/HasPermission.mjs";
3
- describe("HasPermission", () => {
4
- it("passes on exact match", () => {
5
- const rule = new HasPermission("project:1.perm:read");
6
- const attrs = new Map([["permissions", ["project:1.perm:read"]]]);
7
- expect(rule.verify(attrs)).toBe(true);
8
- });
9
- it("fails when permission is missing", () => {
10
- const rule = new HasPermission("project:1.perm:write");
11
- const attrs = new Map([["permissions", ["project:1.perm:read"]]]);
12
- expect(rule.verify(attrs)).toBe(false);
13
- });
14
- it("matches wildcard * (permit all)", () => {
15
- const rule = new HasPermission("project:1.perm:read");
16
- const attrs = new Map([["permissions", ["*"]]]);
17
- expect(rule.verify(attrs)).toBe(true);
18
- });
19
- it("matches prefix wildcard: project:*.perm:read", () => {
20
- const rule = new HasPermission("project:123.perm:read");
21
- const attrs = new Map([["permissions", ["project:*.perm:read"]]]);
22
- expect(rule.verify(attrs)).toBe(true);
23
- });
24
- it("matches suffix wildcard: project:1.perm:*", () => {
25
- const rule = new HasPermission("project:1.perm:read");
26
- const attrs = new Map([["permissions", ["project:1.perm:*"]]]);
27
- expect(rule.verify(attrs)).toBe(true);
28
- });
29
- it("is case insensitive", () => {
30
- const rule = new HasPermission("Project:1.Perm:Read");
31
- const attrs = new Map([["permissions", ["project:1.perm:read"]]]);
32
- expect(rule.verify(attrs)).toBe(true);
33
- });
34
- it("checks permissions from roles", () => {
35
- const rule = new HasPermission("project:1.perm:read");
36
- const attrs = new Map([
37
- ["permissions", []],
38
- ["roles", [{ name: "admin", permissions: ["project:1.perm:read"] }]],
39
- ]);
40
- expect(rule.verify(attrs)).toBe(true);
41
- });
42
- it("fails when neither permissions nor roles match", () => {
43
- const rule = new HasPermission("project:1.perm:write");
44
- const attrs = new Map([
45
- ["permissions", ["project:1.perm:read"]],
46
- ["roles", [{ name: "viewer", permissions: ["project:1.perm:read"] }]],
47
- ]);
48
- expect(rule.verify(attrs)).toBe(false);
49
- });
50
- it("has ruleType 'permission'", () => {
51
- const rule = new HasPermission("project:1.perm:read");
52
- expect(rule.ruleType).toBe("permission");
53
- });
54
- it("has code 'no_permission'", () => {
55
- const rule = new HasPermission("project:1.perm:read");
56
- expect(rule.code).toBe("no_permission");
57
- });
58
- describe("multi-star wildcard rejection", () => {
59
- it("does NOT match when granted permission has 2+ wildcards — over-granting via dropped suffix", () => {
60
- // Bug: "resource*action*required" split("*") → ["resource","action","required"]
61
- // Destructuring const [prefix, suffix] = ... takes only "resource" and "action",
62
- // silently dropping "required". The check becomes endsWith("action") instead of
63
- // endsWith("required"), so "resourceXaction" incorrectly matches even though
64
- // the granted permission requires a "required" segment after the second wildcard.
65
- // Fix: reject any granted permission containing 2+ wildcards (return false).
66
- const rule = new HasPermission("resourceXaction");
67
- const attrs = new Map([["permissions", ["resource*action*required"]]]);
68
- // "resourceXaction" does NOT satisfy "resource*action*required" (missing "required")
69
- // but the buggy code returns true because "required" is dropped from suffix
70
- expect(rule.verify(attrs)).toBe(false);
71
- });
72
- it("does NOT match when granted permission has 2+ wildcards (posts.*.write.* vs posts.123.read.all)", () => {
73
- // Additional case: wildcard in the middle — cross-action access must not be granted
74
- const rule = new HasPermission("posts.123.read.all");
75
- const attrs = new Map([["permissions", ["posts.*.write.*"]]]);
76
- expect(rule.verify(attrs)).toBe(false);
77
- });
78
- it("does NOT match when granted permission has 3 wildcards", () => {
79
- // "a*b*c*d" split → ["a","b","c","d"]; prefix="a", suffix="b"; "c","d" dropped.
80
- // "aXb" incorrectly matches when "a*b*c*d" should require segments c and d.
81
- const rule = new HasPermission("aXb");
82
- const attrs = new Map([["permissions", ["a*b*c*d"]]]);
83
- expect(rule.verify(attrs)).toBe(false);
84
- });
85
- });
86
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=HasScope.test.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"HasScope.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/HasScope.test.mts"],"names":[],"mappings":""}
@@ -1,42 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { HasScope } from "#/rules/HasScope.mjs";
3
- describe("HasScope", () => {
4
- it("passes when scope matches exactly", () => {
5
- const rule = new HasScope("read:user");
6
- const attrs = new Map([["scopes", ["read:user"]]]);
7
- expect(rule.verify(attrs)).toBe(true);
8
- });
9
- it("fails when scope does not match", () => {
10
- const rule = new HasScope("write:user");
11
- const attrs = new Map([["scopes", ["read:user"]]]);
12
- expect(rule.verify(attrs)).toBe(false);
13
- });
14
- it("normalizes scope without action to read:{resource}", () => {
15
- const rule = new HasScope("read:user");
16
- const attrs = new Map([["scopes", ["user"]]]);
17
- expect(rule.verify(attrs)).toBe(true);
18
- });
19
- it("is case insensitive", () => {
20
- const rule = new HasScope("READ:User");
21
- const attrs = new Map([["scopes", ["read:user"]]]);
22
- expect(rule.verify(attrs)).toBe(true);
23
- });
24
- it("fails when scopes are empty", () => {
25
- const rule = new HasScope("read:user");
26
- const attrs = new Map([["scopes", []]]);
27
- expect(rule.verify(attrs)).toBe(false);
28
- });
29
- it("fails when scopes key is missing", () => {
30
- const rule = new HasScope("read:user");
31
- const attrs = new Map();
32
- expect(rule.verify(attrs)).toBe(false);
33
- });
34
- it("has ruleType 'scope'", () => {
35
- const rule = new HasScope("read:user");
36
- expect(rule.ruleType).toBe("scope");
37
- });
38
- it("has code 'invalid_scope'", () => {
39
- const rule = new HasScope("read:user");
40
- expect(rule.code).toBe("invalid_scope");
41
- });
42
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=_sharedValidation.test.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"_sharedValidation.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/_sharedValidation.test.mts"],"names":[],"mappings":""}
@@ -1,325 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { applyCompare, COMPARE_OPS, computeValuesKey, requireAttrName, requireCompareOp, requireHomogeneousLiteralArray, requireLiteralValue, requireNumber, requireOptionalGroup, } from "#/rules/_sharedValidation.mjs";
3
- // ---------------------------------------------------------------------------
4
- // requireAttrName
5
- // ---------------------------------------------------------------------------
6
- describe("requireAttrName", () => {
7
- it("returns the input string when value is a valid non-empty string", () => {
8
- expect(requireAttrName("MyClass", "a", "role")).toBe("role");
9
- });
10
- it("returns the input string for fieldName 'b'", () => {
11
- expect(requireAttrName("MyClass", "b", "score")).toBe("score");
12
- });
13
- it("throws with className and fieldName 'a' in message when value is undefined", () => {
14
- expect(() => requireAttrName("MyClass", "a", undefined)).toThrow(/MyClass.*'a'.*non-empty string.*got undefined/);
15
- });
16
- it("throws with className and fieldName 'b' in message when value is undefined", () => {
17
- expect(() => requireAttrName("MyClass", "b", undefined)).toThrow(/MyClass.*'b'.*non-empty string.*got undefined/);
18
- });
19
- it("throws with 'empty string' in message when value is ''", () => {
20
- expect(() => requireAttrName("MyClass", "a", "")).toThrow(/MyClass.*'a'.*got empty string/);
21
- });
22
- it("throws with 'got number' in message when value is 42", () => {
23
- expect(() => requireAttrName("MyClass", "a", 42)).toThrow(/MyClass.*'a'.*got number/);
24
- });
25
- it("throws with 'null' in message when value is null", () => {
26
- expect(() => requireAttrName("MyClass", "a", null)).toThrow(/null/);
27
- });
28
- it("throws when value contains ':' (reserved as ruleType separator)", () => {
29
- // Regression guard for an authorization-weakening bug: with ':' allowed,
30
- // (a="x:y", b="z") and (a="x", b="y:z") would both produce the same
31
- // `attr_pair_equal:x:y:z` default ruleType and be silently OR-combined
32
- // by the evaluator. Fail-fast at construction instead.
33
- expect(() => requireAttrName("MyClass", "a", "name:space")).toThrow(/must not contain ':'.*got 'name:space'/);
34
- });
35
- });
36
- // ---------------------------------------------------------------------------
37
- // requireLiteralValue
38
- // ---------------------------------------------------------------------------
39
- describe("requireLiteralValue", () => {
40
- it("returns the value when it is a string", () => {
41
- expect(requireLiteralValue("MyClass", "v", "hello")).toBe("hello");
42
- });
43
- it("returns the value when it is a number", () => {
44
- expect(requireLiteralValue("MyClass", "v", 42)).toBe(42);
45
- });
46
- it("returns the value when it is a boolean", () => {
47
- expect(requireLiteralValue("MyClass", "v", false)).toBe(false);
48
- });
49
- it("throws with 'null' in message when value is null", () => {
50
- expect(() => requireLiteralValue("MyClass", "v", null)).toThrow(/null/);
51
- });
52
- it("throws with 'undefined' in message when value is undefined", () => {
53
- expect(() => requireLiteralValue("MyClass", "v", undefined)).toThrow(/undefined/);
54
- });
55
- it("throws with 'object' in message when value is a plain object", () => {
56
- expect(() => requireLiteralValue("MyClass", "v", {})).toThrow(/object/);
57
- });
58
- it("throws with 'object' in message when value is an array (typeof [] === 'object')", () => {
59
- expect(() => requireLiteralValue("MyClass", "v", [])).toThrow(/object/);
60
- });
61
- it("throws on NaN (NaN === NaN is false; would invert Equal/NotEqual rules)", () => {
62
- // Regression guard for an authorization-weakening bug: with NaN accepted,
63
- // AttrLiteralNotEqual({ v: NaN }) would always pass for any numeric
64
- // attribute because `x !== NaN` is always true. Must fail-fast in
65
- // construction instead.
66
- expect(() => requireLiteralValue("MyClass", "v", Number.NaN)).toThrow(/must not be NaN/);
67
- });
68
- });
69
- // ---------------------------------------------------------------------------
70
- // requireNumber
71
- // ---------------------------------------------------------------------------
72
- describe("requireNumber", () => {
73
- it("returns 0 (zero)", () => {
74
- expect(requireNumber("MyClass", "v", 0)).toBe(0);
75
- });
76
- it("returns -0 (negative zero)", () => {
77
- expect(requireNumber("MyClass", "v", -0)).toBe(-0);
78
- });
79
- it("returns a large finite number", () => {
80
- expect(requireNumber("MyClass", "v", 1e308)).toBe(1e308);
81
- });
82
- it("returns Infinity without throwing (Infinity is a valid number per spec)", () => {
83
- expect(requireNumber("MyClass", "v", Infinity)).toBe(Infinity);
84
- });
85
- it("returns -Infinity without throwing", () => {
86
- expect(requireNumber("MyClass", "v", -Infinity)).toBe(-Infinity);
87
- });
88
- it("throws on NaN with message matching /must not be NaN/", () => {
89
- expect(() => requireNumber("MyClass", "v", NaN)).toThrow(/must not be NaN/);
90
- });
91
- it("throws on a string '42' with message containing 'got string'", () => {
92
- expect(() => requireNumber("MyClass", "v", "42")).toThrow(/got string/);
93
- });
94
- it("throws on null with message containing 'null'", () => {
95
- expect(() => requireNumber("MyClass", "v", null)).toThrow(/null/);
96
- });
97
- it("throws on undefined with message containing 'undefined'", () => {
98
- expect(() => requireNumber("MyClass", "v", undefined)).toThrow(/undefined/);
99
- });
100
- });
101
- // ---------------------------------------------------------------------------
102
- // requireHomogeneousLiteralArray
103
- // ---------------------------------------------------------------------------
104
- describe("requireHomogeneousLiteralArray", () => {
105
- it("returns a non-empty all-string array", () => {
106
- const arr = ["a", "b", "c"];
107
- expect(requireHomogeneousLiteralArray("MyClass", arr)).toEqual(arr);
108
- });
109
- it("returns a non-empty all-number array", () => {
110
- const arr = [1, 2, 3];
111
- expect(requireHomogeneousLiteralArray("MyClass", arr)).toEqual(arr);
112
- });
113
- it("returns a non-empty all-boolean array", () => {
114
- const arr = [true, false];
115
- expect(requireHomogeneousLiteralArray("MyClass", arr)).toEqual(arr);
116
- });
117
- it("throws with 'got string' when value is not an array", () => {
118
- expect(() => requireHomogeneousLiteralArray("MyClass", "abc")).toThrow(/got string/);
119
- });
120
- it("throws with 'undefined' when value is undefined", () => {
121
- expect(() => requireHomogeneousLiteralArray("MyClass", undefined)).toThrow(/undefined/);
122
- });
123
- it("throws with 'null' when value is null", () => {
124
- expect(() => requireHomogeneousLiteralArray("MyClass", null)).toThrow(/null/);
125
- });
126
- it("throws with 'non-empty array' on empty array", () => {
127
- expect(() => requireHomogeneousLiteralArray("MyClass", [])).toThrow(/non-empty array/);
128
- });
129
- it("throws with 'null' when an element is null", () => {
130
- expect(() => requireHomogeneousLiteralArray("MyClass", [null])).toThrow(/null/);
131
- });
132
- it("throws with 'undefined' when an element is undefined", () => {
133
- expect(() => requireHomogeneousLiteralArray("MyClass", [undefined])).toThrow(/undefined/);
134
- });
135
- it("throws with 'object' when an element is an object", () => {
136
- expect(() => requireHomogeneousLiteralArray("MyClass", [{}])).toThrow(/object/);
137
- });
138
- it("throws with mixed types message for ['a', 1] (left-to-right Set insertion order)", () => {
139
- expect(() => requireHomogeneousLiteralArray("MyClass", ["a", 1])).toThrow(/mixed types \[string, number\]/);
140
- });
141
- it("throws when an element is NaN (would silently mismatch AttrLiteralIn/NotIn)", () => {
142
- // Regression guard: AttrLiteralIn uses strict equality against each element;
143
- // a NaN element could never match any attribute value because NaN !== NaN.
144
- // Must fail-fast at construction rather than ship an always-false element.
145
- expect(() => requireHomogeneousLiteralArray("MyClass", [1, Number.NaN, 3])).toThrow(/must not contain NaN/);
146
- });
147
- });
148
- // ---------------------------------------------------------------------------
149
- // requireCompareOp
150
- // ---------------------------------------------------------------------------
151
- describe("requireCompareOp", () => {
152
- it("returns 'lt' for valid op 'lt'", () => {
153
- expect(requireCompareOp("MyClass", "lt")).toBe("lt");
154
- });
155
- it("returns 'le' for valid op 'le'", () => {
156
- expect(requireCompareOp("MyClass", "le")).toBe("le");
157
- });
158
- it("returns 'gt' for valid op 'gt'", () => {
159
- expect(requireCompareOp("MyClass", "gt")).toBe("gt");
160
- });
161
- it("returns 'ge' for valid op 'ge'", () => {
162
- expect(requireCompareOp("MyClass", "ge")).toBe("ge");
163
- });
164
- it("throws on invalid string 'eq' with message listing valid ops joined by ' | '", () => {
165
- expect(() => requireCompareOp("MyClass", "eq")).toThrow(/lt \| le \| gt \| ge/);
166
- });
167
- it("throws on empty string '' with message listing valid ops", () => {
168
- expect(() => requireCompareOp("MyClass", "")).toThrow(/lt \| le \| gt \| ge/);
169
- });
170
- it("throws on symbolic string '<' with message listing valid ops", () => {
171
- expect(() => requireCompareOp("MyClass", "<")).toThrow(/lt \| le \| gt \| ge/);
172
- });
173
- it("throws on number 42 with message containing the type label", () => {
174
- expect(() => requireCompareOp("MyClass", 42)).toThrow(/got number/);
175
- });
176
- it("throws on null with message containing the type label", () => {
177
- expect(() => requireCompareOp("MyClass", null)).toThrow(/null/);
178
- });
179
- it("throws on undefined with message containing the type label", () => {
180
- expect(() => requireCompareOp("MyClass", undefined)).toThrow(/undefined/);
181
- });
182
- });
183
- // ---------------------------------------------------------------------------
184
- // requireOptionalGroup
185
- // ---------------------------------------------------------------------------
186
- describe("requireOptionalGroup", () => {
187
- it("returns undefined when value is undefined", () => {
188
- expect(requireOptionalGroup("MyClass", undefined)).toBeUndefined();
189
- });
190
- it("returns the input string when value is a non-empty string", () => {
191
- expect(requireOptionalGroup("MyClass", "my-group")).toBe("my-group");
192
- });
193
- it("throws with 'empty string' message on ''", () => {
194
- expect(() => requireOptionalGroup("MyClass", "")).toThrow(/empty string/);
195
- });
196
- it("throws with 'got number' message on 42", () => {
197
- expect(() => requireOptionalGroup("MyClass", 42)).toThrow(/got number/);
198
- });
199
- it("throws with 'null' in message on null", () => {
200
- expect(() => requireOptionalGroup("MyClass", null)).toThrow(/null/);
201
- });
202
- });
203
- // ---------------------------------------------------------------------------
204
- // computeValuesKey
205
- // ---------------------------------------------------------------------------
206
- describe("computeValuesKey", () => {
207
- it("returns a string matching /{type}:{count}:{8-hex-chars}/ format", () => {
208
- expect(computeValuesKey(["admin"])).toMatch(/^(string|number|boolean):[0-9]+:[0-9a-f]{8}$/);
209
- });
210
- it("sort determinism: same content in different order produces the same key", () => {
211
- expect(computeValuesKey(["b", "a"])).toBe(computeValuesKey(["a", "b"]));
212
- });
213
- it("count discrimination: ['a'] !== ['a', 'b'] (distinct content → distinct key)", () => {
214
- expect(computeValuesKey(["a"])).not.toBe(computeValuesKey(["a", "b"]));
215
- });
216
- it("dedup parity: ['a'] === ['a', 'a'] (duplicates do not affect the Set-based rule behavior, so they must not affect ruleType either)", () => {
217
- // AttrLiteralIn / AttrLiteralNotIn use `new Set(values)` for verify(),
218
- // so duplicate elements have no semantic effect. The ruleType must
219
- // mirror that — otherwise two logically-equivalent rules would be
220
- // placed in different evaluator groups and be AND-combined, when
221
- // the caller intended them to be OR-combined (shared ruleType).
222
- expect(computeValuesKey(["a"])).toBe(computeValuesKey(["a", "a"]));
223
- });
224
- it("content discrimination: ['a', 'b'] !== ['a', 'c'] (hash differs)", () => {
225
- expect(computeValuesKey(["a", "b"])).not.toBe(computeValuesKey(["a", "c"]));
226
- });
227
- it("type segment correctness: string array starts with 'string:'", () => {
228
- expect(computeValuesKey(["admin"])).toMatch(/^string:/);
229
- });
230
- it("type segment correctness: number array starts with 'number:'", () => {
231
- expect(computeValuesKey([1])).toMatch(/^number:/);
232
- });
233
- it("type segment correctness: boolean array starts with 'boolean:'", () => {
234
- expect(computeValuesKey([true])).toMatch(/^boolean:/);
235
- });
236
- it("type divergence: computeValuesKey(['1']) !== computeValuesKey([1]) (type segment differs)", () => {
237
- expect(computeValuesKey(["1"])).not.toBe(computeValuesKey([1]));
238
- });
239
- it("separator collision guard: ['a,b', 'c'] !== ['a', 'b,c'] (values containing commas do not collide)", () => {
240
- // Regression guard: a naive implementation of the form
241
- // values.map(String).sort().join(",")
242
- // would produce the same string "a,b,c" for both inputs and thus the
243
- // same ruleType. That would cause the evaluator to OR what the caller
244
- // meant as two independent set constraints, weakening authorization.
245
- // The canonical form JSON.stringify-es each element before joining,
246
- // so quotes/escapes disambiguate the element boundary.
247
- expect(computeValuesKey(["a,b", "c"])).not.toBe(computeValuesKey(["a", "b,c"]));
248
- });
249
- it("pure / referentially transparent: two calls with the same input return the same string", () => {
250
- const input = ["x", "y", "z"];
251
- expect(computeValuesKey(input)).toBe(computeValuesKey(input));
252
- });
253
- });
254
- // ---------------------------------------------------------------------------
255
- // applyCompare
256
- // ---------------------------------------------------------------------------
257
- describe("applyCompare", () => {
258
- describe("op=lt", () => {
259
- it("returns true when left < right (1 < 2)", () => {
260
- expect(applyCompare("lt", 1, 2)).toBe(true);
261
- });
262
- it("returns false when left === right (2 < 2 is false)", () => {
263
- expect(applyCompare("lt", 2, 2)).toBe(false);
264
- });
265
- it("returns false when left > right (3 < 2 is false)", () => {
266
- expect(applyCompare("lt", 3, 2)).toBe(false);
267
- });
268
- it("edge: -Infinity < 0 returns true", () => {
269
- expect(applyCompare("lt", -Infinity, 0)).toBe(true);
270
- });
271
- });
272
- describe("op=le", () => {
273
- it("returns true when left < right (1 <= 2)", () => {
274
- expect(applyCompare("le", 1, 2)).toBe(true);
275
- });
276
- it("returns true when left === right (2 <= 2)", () => {
277
- expect(applyCompare("le", 2, 2)).toBe(true);
278
- });
279
- it("returns false when left > right (3 <= 2 is false)", () => {
280
- expect(applyCompare("le", 3, 2)).toBe(false);
281
- });
282
- it("edge: 0 >= -0 returns true (ge symmetry check; 0 <= -0 also true)", () => {
283
- expect(applyCompare("le", 0, -0)).toBe(true);
284
- });
285
- });
286
- describe("op=gt", () => {
287
- it("returns true when left > right (3 > 2)", () => {
288
- expect(applyCompare("gt", 3, 2)).toBe(true);
289
- });
290
- it("returns false when left === right (2 > 2 is false)", () => {
291
- expect(applyCompare("gt", 2, 2)).toBe(false);
292
- });
293
- it("returns false when left < right (1 > 2 is false)", () => {
294
- expect(applyCompare("gt", 1, 2)).toBe(false);
295
- });
296
- it("edge: Infinity > 1e308 returns true", () => {
297
- expect(applyCompare("gt", Infinity, 1e308)).toBe(true);
298
- });
299
- });
300
- describe("op=ge", () => {
301
- it("returns true when left > right (3 >= 2)", () => {
302
- expect(applyCompare("ge", 3, 2)).toBe(true);
303
- });
304
- it("returns true when left === right (2 >= 2)", () => {
305
- expect(applyCompare("ge", 2, 2)).toBe(true);
306
- });
307
- it("returns false when left < right (1 >= 2 is false)", () => {
308
- expect(applyCompare("ge", 1, 2)).toBe(false);
309
- });
310
- it("edge: 0 >= -0 returns true", () => {
311
- expect(applyCompare("ge", 0, -0)).toBe(true);
312
- });
313
- });
314
- });
315
- // ---------------------------------------------------------------------------
316
- // COMPARE_OPS
317
- // ---------------------------------------------------------------------------
318
- describe("COMPARE_OPS", () => {
319
- it("exposes exactly 4 values", () => {
320
- expect(COMPARE_OPS).toHaveLength(4);
321
- });
322
- it("contains 'lt', 'le', 'gt', 'ge' in order", () => {
323
- expect(COMPARE_OPS).toEqual(["lt", "le", "gt", "ge"]);
324
- });
325
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=ResourceActionPermissionRuleCollector.test.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ResourceActionPermissionRuleCollector.test.d.mts","sourceRoot":"","sources":["../../../../src/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.mts"],"names":[],"mappings":""}
@@ -1,27 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { ResourceActionPermissionRuleCollector } from "#/rules/collectors/ResourceActionPermissionRuleCollector.mjs";
3
- describe("ResourceActionPermissionRuleCollector", () => {
4
- const collector = new ResourceActionPermissionRuleCollector();
5
- it("creates a HasPermission rule with resource.perm:action", async () => {
6
- const ctx = {
7
- payload: {},
8
- resource: { raw: "document:12345", resourceType: "document", resourceId: "12345" },
9
- action: "read",
10
- };
11
- const rules = await collector.collect(ctx);
12
- expect(rules).toHaveLength(1);
13
- expect(rules[0].ruleType).toBe("permission");
14
- const attrs = new Map([["permissions", ["document:12345.perm:read"]]]);
15
- expect(rules[0].verify(attrs)).toBe(true);
16
- });
17
- it("creates correct permission for nested resources", async () => {
18
- const ctx = {
19
- payload: {},
20
- resource: { raw: "project:1.member", resourceType: "project_member" },
21
- action: "update",
22
- };
23
- const rules = await collector.collect(ctx);
24
- const attrs = new Map([["permissions", ["project:1.member.perm:update"]]]);
25
- expect(rules[0].verify(attrs)).toBe(true);
26
- });
27
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=ResourceActionScopeRuleCollector.test.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ResourceActionScopeRuleCollector.test.d.mts","sourceRoot":"","sources":["../../../../src/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.mts"],"names":[],"mappings":""}
@@ -1,48 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { ResourceActionScopeRuleCollector } from "#/rules/collectors/ResourceActionScopeRuleCollector.mjs";
3
- const makeContext = (resourceType, action, scope) => ({
4
- payload: { ...(scope !== undefined ? { scope } : {}) },
5
- resource: { raw: `${resourceType}:1`, resourceType, resourceId: "1" },
6
- action,
7
- });
8
- describe("ResourceActionScopeRuleCollector", () => {
9
- const collector = new ResourceActionScopeRuleCollector();
10
- it("creates a HasScope rule with action:resourceType", async () => {
11
- const rules = await collector.collect(makeContext("document", "read", "read:document"));
12
- expect(rules).toHaveLength(1);
13
- expect(rules[0].ruleType).toBe("scope");
14
- const attrs = new Map([["scopes", ["read:document"]]]);
15
- expect(rules[0].verify(attrs)).toBe(true);
16
- });
17
- it("creates correct scope for nested resource types", async () => {
18
- const ctx = {
19
- payload: { scope: "update:project_member" },
20
- resource: { raw: "project:1.member:2", resourceType: "project_member", resourceId: "2" },
21
- action: "update",
22
- };
23
- const rules = await collector.collect(ctx);
24
- expect(rules).toHaveLength(1);
25
- const attrs = new Map([["scopes", ["update:project_member"]]]);
26
- expect(rules[0].verify(attrs)).toBe(true);
27
- });
28
- it("returns no rules when payload has no scope claim", async () => {
29
- const ctx = makeContext("document", "read");
30
- // payload has no scope — simulates DID JWT
31
- const rules = await collector.collect(ctx);
32
- expect(rules).toHaveLength(0);
33
- });
34
- it("returns HasScope rule when payload has scope claim", async () => {
35
- const ctx = {
36
- payload: { scope: "read:document" },
37
- resource: { raw: "document:1", resourceType: "document", resourceId: "1" },
38
- action: "read",
39
- };
40
- const rules = await collector.collect(ctx);
41
- expect(rules).toHaveLength(1);
42
- expect(rules[0].ruleType).toBe("scope");
43
- });
44
- it("still generates HasScope rule when scope is empty string", async () => {
45
- const rules = await collector.collect(makeContext("document", "read", ""));
46
- expect(rules).toHaveLength(1);
47
- });
48
- });