@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
package/README.ja.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  auth.policy-verifier 向けの組み込み attribute collector、rule collector、および resource parser です。
4
4
 
5
+ **Runtime:** Node.js 22+。本パッケージは `ruleType` の安定ハッシュ用に `node:crypto` を利用します。browser / edge ランタイム対応は今後の課題として別 issue で追跡します。
6
+
5
7
  ## インストール
6
8
 
7
9
  ```bash
@@ -54,6 +56,8 @@ new HasScope(scope: string)
54
56
 
55
57
  ### AttrMatchRule
56
58
 
59
+ **非推奨。** 代わりに [`AttrPairEqual`](#attrpairequal) を使用してください。`AttrMatchRule` は `AttrPairEqual` を継承した薄いラッパークラスとして残されており、後方互換のため旧来の `ruleType`(`attr_match:${a}:${b}`)と旧来の `message` 文言を保持します。型 `AttrMatchRuleConfig` は `AttrPairEqualConfig` の型エイリアスです。将来のメジャーバージョンで削除されます。
60
+
57
61
  ```ts
58
62
  new AttrMatchRule({ a: string, b: string, group?: string })
59
63
  ```
@@ -63,6 +67,103 @@ new AttrMatchRule({ a: string, b: string, group?: string })
63
67
  - 純粋な述語です。`CollectorContext` を参照しません。比較対象の値はプロジェクト側の上流 `AttributeCollector` が attrs に格納し、プロジェクト側の `RuleCollector` でこの Rule を構築します。
64
68
  - `ruleType` の既定値は `"attr_match:${a}:${b}"` です。評価器は `ruleType` 内で OR、`ruleType` 間で AND を取るので、この既定値により異なる2つの比較は AND(両方必要)として扱われます。2つの比較を OR 結合したい場合(例「DID または email で一致」)は、両方の Rule に同じ `group` を指定してください。その `group` 値が `ruleType` として使われます。
65
69
 
70
+ ## Attribute Comparison Rules
71
+
72
+ 属性比較 Rule 群は 2 軸のマトリクスで構成されます: **family**(Literal または Pair)と **operator**(Equal、NotEqual、In、NotIn、Compare)。
73
+
74
+ - **Literal** Rule は、単一の名前付き属性を、コンストラクタで与えられた静的な値(または値集合)と比較します。
75
+ - **Pair** Rule は、評価時に `Attributes` から解決される 2 つの名前付き属性を互いに比較します。
76
+ - `In` / `NotIn` のバリアントは Literal family のみ提供します。集合に対する Pair 比較は有限リストへ自然に一般化できないため、`AttrPairIn` / `AttrPairNotIn` は意図的に存在しません。
77
+
78
+ | Family | Equal | NotEqual | In | NotIn | Compare |
79
+ | ------- | ------------------ | --------------------- | --------------- | ------------------ | -------------------- |
80
+ | Literal | `AttrLiteralEqual` | `AttrLiteralNotEqual` | `AttrLiteralIn` | `AttrLiteralNotIn` | `AttrLiteralCompare` |
81
+ | Pair | `AttrPairEqual` | `AttrPairNotEqual` | — | — | `AttrPairCompare` |
82
+
83
+ ### AttrLiteralEqual
84
+
85
+ ```ts
86
+ new AttrLiteralEqual({ a: string, v: string | number | boolean, group?: string })
87
+ ```
88
+
89
+ - `code`: `"attr_not_equal"`。
90
+ - 既定の `ruleType`: `` `attr_literal_equal:${a}:${typeof v}:${String(v)}` ``。`typeof v` セグメントは、文字列化すると同じになる異型リテラル(例: `true` と `"true"`)が同じ `ruleType` に畳み込まれるのを防ぎます。
91
+ - `attrs.get(a)` が `v` と同じ型で厳密等価のときに通過します。型の強制変換は行いません。
92
+
93
+ ### AttrLiteralNotEqual
94
+
95
+ ```ts
96
+ new AttrLiteralNotEqual({ a: string, v: string | number | boolean, group?: string })
97
+ ```
98
+
99
+ - `code`: `"attr_equal"`。
100
+ - 既定の `ruleType`: `` `attr_literal_not_equal:${a}:${typeof v}:${String(v)}` ``。`typeof v` セグメントは異型リテラル間の衝突を防ぎます(`AttrLiteralEqual` と同じ理由)。
101
+ - `attrs.get(a)` が `v` と同じ型で厳密等価でないときに通過します。欠落または型不一致は `false`(safe-deny)を返します。
102
+
103
+ ### AttrLiteralIn
104
+
105
+ ```ts
106
+ new AttrLiteralIn({ a: string, values: (string | number | boolean)[], group?: string })
107
+ ```
108
+
109
+ - `code`: `"attr_not_in_set"`。
110
+ - 既定の `ruleType`: `` `attr_literal_in:${a}:${type}:${count}:${hashPrefix}` `` — `count` は重複除去後の要素数、`hashPrefix` は重複除去かつソート済みの `values` を文字列化した内容に対する SHA-256 の先頭 8 桁。同じ `a` と内容上等価な `values`(順序・重複を無視)を持つ 2 つのインスタンスは同じ `ruleType` を持ち、評価器で OR 結合されます。
111
+ - `values` は非空かつ同種の配列(`string[]`、`number[]`、`boolean[]` のいずれか)でなければなりません。`attrs.get(a)` が集合に含まれるときに通過します。`values` 内の重複要素は Rule の挙動に影響しません(内部では `Set` として扱われます)。
112
+
113
+ ### AttrLiteralNotIn
114
+
115
+ ```ts
116
+ new AttrLiteralNotIn({ a: string, values: (string | number | boolean)[], group?: string })
117
+ ```
118
+
119
+ - `code`: `"attr_in_set"`。
120
+ - 既定の `ruleType`: `` `attr_literal_not_in:${a}:${type}:${count}:${hashPrefix}` `` — `AttrLiteralIn` と同じ、重複除去済みの安定ハッシュ方式。
121
+ - `values` は非空かつ同種の配列。`attrs.get(a)` が集合に含まれないときに通過します。`values` 内の重複要素は Rule の挙動に影響しません。
122
+
123
+ ### AttrLiteralCompare
124
+
125
+ ```ts
126
+ new AttrLiteralCompare({ a: string, op: "lt" | "le" | "gt" | "ge", v: number, group?: string })
127
+ ```
128
+
129
+ - `code`: `"attr_compare_violated"`。
130
+ - 既定の `ruleType`: `` `attr_literal_compare:${a}:${op}:${String(v)}` ``。
131
+ - `attrs.get(a)` が数値であり `a op v` を満たすときに通過します。`v` が NaN の場合はコンストラクタ時点で拒否されます。属性側が NaN の場合は常に `false` を返します。
132
+
133
+ ### AttrPairEqual
134
+
135
+ ```ts
136
+ new AttrPairEqual({ a: string, b: string, group?: string })
137
+ ```
138
+
139
+ - `code`: `"attr_mismatch"`。
140
+ - 既定の `ruleType`: `` `attr_pair_equal:${a}:${b}` ``。
141
+ - `attrs.get(a)` と `attrs.get(b)` がいずれも非空文字列かつ厳密等価のときに通過します。非推奨となった `AttrMatchRule` の後継です。
142
+
143
+ ### AttrPairNotEqual
144
+
145
+ ```ts
146
+ new AttrPairNotEqual({ a: string, b: string, group?: string })
147
+ ```
148
+
149
+ - `code`: `"attr_match"`。
150
+ - 既定の `ruleType`: `` `attr_pair_not_equal:${a}:${b}` ``。
151
+ - `attrs.get(a)` と `attrs.get(b)` がいずれも非空文字列かつ厳密等価でないときに通過します。欠落・空文字列・非文字列はすべて `false`(safe-deny)を返します。
152
+
153
+ ### AttrPairCompare
154
+
155
+ ```ts
156
+ new AttrPairCompare({ a: string, op: "lt" | "le" | "gt" | "ge", b: string, group?: string })
157
+ ```
158
+
159
+ - `code`: `"attr_compare_violated"`。
160
+ - 既定の `ruleType`: `` `attr_pair_compare:${a}:${op}:${b}` ``。
161
+ - `attrs.get(a)` と `attrs.get(b)` がいずれも数値で `a op b` を満たすときに通過します。どちらかが NaN の場合は `false` を返します(JS の比較セマンティクス)。
162
+
163
+ ### グルーピング: 既定は AND、OR にするには `group` を指定
164
+
165
+ 属性比較 Rule はすべて、上記 `AttrMatchRule` と同じグルーピング規則に従います。既定では各 Rule の `ruleType` は識別用パラメータから導出されるため、異なる要件は評価器によって AND 結合されます。2 つの Rule に同じ `group` 文字列を渡すと、両者は同じ `ruleType` を持つようになり、評価器は OR 結合します(どちらか一方が成立すれば要件を満たす)。
166
+
66
167
  ## Rule Collectors
67
168
 
68
169
  | 名前 | 導出する permission / scope | 返り値 |
@@ -110,5 +211,6 @@ import { builtinCollectorsModule } from "@o3co/auth.policy-verifier.builtins";
110
211
 
111
212
  ## 関連
112
213
 
113
- - [`@o3co/auth.policy-verifier.core`](../core/README.md) — コアインターフェースと attribute 定数
114
- - [auth.policy-verifier ルート README](../../README.md) — 完全なセットアップと設定のリファレンス
214
+ - [拡張ガイド (`docs/extending.ja.md`)](../../docs/extending.ja.md) — カスタム `Rule` / `AttributeCollector` の書き方と、`builtins` が基本セットとして位置づけられている理由
215
+ - [`@o3co/auth.policy-verifier.core`](../core/README.ja.md) — コアインターフェースと attribute 定数
216
+ - [auth.policy-verifier ルート README](../../README.ja.md) — 完全なセットアップと設定のリファレンス
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Built-in attribute collectors, rule collectors, and resource parser for auth.policy-verifier.
4
4
 
5
+ **Runtime:** Node.js 22+. This package uses `node:crypto` for stable rule-type hashing; browser / edge runtime support is tracked as future work.
6
+
5
7
  ## Install
6
8
 
7
9
  ```bash
@@ -54,6 +56,8 @@ new HasScope(scope: string)
54
56
 
55
57
  ### AttrMatchRule
56
58
 
59
+ **Deprecated.** Use [`AttrPairEqual`](#attrpairequal) instead. `AttrMatchRule` is kept as a thin wrapper class that extends `AttrPairEqual` and preserves the legacy `ruleType` (`attr_match:${a}:${b}`) and legacy `message` wording for backward compatibility. The type `AttrMatchRuleConfig` is a type alias of `AttrPairEqualConfig`. It will be removed in a future major version.
60
+
57
61
  ```ts
58
62
  new AttrMatchRule({ a: string, b: string, group?: string })
59
63
  ```
@@ -63,6 +67,103 @@ new AttrMatchRule({ a: string, b: string, group?: string })
63
67
  - Pure predicate — does not read `CollectorContext`. Consuming projects provide the two values to compare through upstream `AttributeCollector`s and wire the rule through their own `RuleCollector`.
64
68
  - `ruleType` defaults to `"attr_match:${a}:${b}"`. The evaluator ORs rules within a `ruleType` and ANDs across different `ruleType`s, so the default ensures two independent comparisons are AND-combined (required together). Pass `group` explicitly when you want two comparisons to be OR-combined (for example, "identify by DID or by email") — both rules then share the provided `group` as their `ruleType`.
65
69
 
70
+ ## Attribute Comparison Rules
71
+
72
+ The attribute comparison rules form a 2 × 5 matrix over two axes: **family** (Literal vs. Pair) and **operator** (Equal, NotEqual, In, NotIn, Compare).
73
+
74
+ - **Literal** rules compare a single named attribute against a static value (or set of values) supplied at construction time.
75
+ - **Pair** rules compare two named attributes resolved from the `Attributes` map at evaluation time.
76
+ - `In` / `NotIn` variants exist for the Literal family only. A pair-over-set operation does not generalize cleanly to a finite list, so `AttrPairIn` / `AttrPairNotIn` are intentionally absent.
77
+
78
+ | Family | Equal | NotEqual | In | NotIn | Compare |
79
+ | ------- | ------------------ | --------------------- | --------------- | ------------------ | -------------------- |
80
+ | Literal | `AttrLiteralEqual` | `AttrLiteralNotEqual` | `AttrLiteralIn` | `AttrLiteralNotIn` | `AttrLiteralCompare` |
81
+ | Pair | `AttrPairEqual` | `AttrPairNotEqual` | — | — | `AttrPairCompare` |
82
+
83
+ ### AttrLiteralEqual
84
+
85
+ ```ts
86
+ new AttrLiteralEqual({ a: string, v: string | number | boolean, group?: string })
87
+ ```
88
+
89
+ - `code`: `"attr_not_equal"`.
90
+ - Default `ruleType`: `` `attr_literal_equal:${a}:${typeof v}:${String(v)}` ``. The `typeof v` segment prevents silent collisions between distinct-type literals that stringify the same way (e.g. `true` vs `"true"`).
91
+ - Passes when `attrs.get(a)` is the same type and strictly equal to `v`. No type coercion.
92
+
93
+ ### AttrLiteralNotEqual
94
+
95
+ ```ts
96
+ new AttrLiteralNotEqual({ a: string, v: string | number | boolean, group?: string })
97
+ ```
98
+
99
+ - `code`: `"attr_equal"`.
100
+ - Default `ruleType`: `` `attr_literal_not_equal:${a}:${typeof v}:${String(v)}` ``. The `typeof v` segment prevents silent collisions between distinct-type literals (same rationale as `AttrLiteralEqual`).
101
+ - Passes when `attrs.get(a)` is the same type as `v` and strictly not equal to it. Missing or wrong-type attributes return `false` (safe-deny).
102
+
103
+ ### AttrLiteralIn
104
+
105
+ ```ts
106
+ new AttrLiteralIn({ a: string, values: (string | number | boolean)[], group?: string })
107
+ ```
108
+
109
+ - `code`: `"attr_not_in_set"`.
110
+ - Default `ruleType`: `` `attr_literal_in:${a}:${type}:${count}:${hashPrefix}` `` — where `count` is the post-deduplication element count and `hashPrefix` is the first 8 hex characters of a SHA-256 over the deduplicated, sorted, stringified values. Two instances with the same `a` and logically equivalent `values` (duplicates and order do not matter) share the same `ruleType` and are OR-combined by the evaluator.
111
+ - `values` must be a non-empty, homogeneous array (`string[]`, `number[]`, or `boolean[]`). Passes when `attrs.get(a)` is in the set. Duplicate elements in `values` are ignored (the rule uses `Set` semantics internally).
112
+
113
+ ### AttrLiteralNotIn
114
+
115
+ ```ts
116
+ new AttrLiteralNotIn({ a: string, values: (string | number | boolean)[], group?: string })
117
+ ```
118
+
119
+ - `code`: `"attr_in_set"`.
120
+ - Default `ruleType`: `` `attr_literal_not_in:${a}:${type}:${count}:${hashPrefix}` `` — same stable, deduplication-aware hash scheme as `AttrLiteralIn`.
121
+ - `values` must be a non-empty, homogeneous array. Passes when `attrs.get(a)` is NOT in the set. Duplicate elements in `values` are ignored.
122
+
123
+ ### AttrLiteralCompare
124
+
125
+ ```ts
126
+ new AttrLiteralCompare({ a: string, op: "lt" | "le" | "gt" | "ge", v: number, group?: string })
127
+ ```
128
+
129
+ - `code`: `"attr_compare_violated"`.
130
+ - Default `ruleType`: `` `attr_literal_compare:${a}:${op}:${String(v)}` ``.
131
+ - Passes when `attrs.get(a)` is a number satisfying `a op v`. NaN as `v` is rejected at construction time. NaN attributes always return `false`.
132
+
133
+ ### AttrPairEqual
134
+
135
+ ```ts
136
+ new AttrPairEqual({ a: string, b: string, group?: string })
137
+ ```
138
+
139
+ - `code`: `"attr_mismatch"`.
140
+ - Default `ruleType`: `` `attr_pair_equal:${a}:${b}` ``.
141
+ - Passes when both `attrs.get(a)` and `attrs.get(b)` are non-empty strings and strictly equal. This is the successor to the deprecated `AttrMatchRule`.
142
+
143
+ ### AttrPairNotEqual
144
+
145
+ ```ts
146
+ new AttrPairNotEqual({ a: string, b: string, group?: string })
147
+ ```
148
+
149
+ - `code`: `"attr_match"`.
150
+ - Default `ruleType`: `` `attr_pair_not_equal:${a}:${b}` ``.
151
+ - Passes when both `attrs.get(a)` and `attrs.get(b)` are non-empty strings and strictly not equal. Missing, empty, or non-string attributes return `false` (safe-deny).
152
+
153
+ ### AttrPairCompare
154
+
155
+ ```ts
156
+ new AttrPairCompare({ a: string, op: "lt" | "le" | "gt" | "ge", b: string, group?: string })
157
+ ```
158
+
159
+ - `code`: `"attr_compare_violated"`.
160
+ - Default `ruleType`: `` `attr_pair_compare:${a}:${op}:${b}` ``.
161
+ - Passes when both `attrs.get(a)` and `attrs.get(b)` are numbers satisfying `a op b`. NaN on either side returns `false` (JS comparison semantics).
162
+
163
+ ### Grouping: AND by default, `group` for OR
164
+
165
+ All attribute comparison rules follow the same grouping semantics described for `AttrMatchRule` above. By default, each rule's `ruleType` is derived from its distinguishing parameters so that distinct requirements are AND-combined by the evaluator. Pass the same `group` string to two rules to give them the same `ruleType` — the evaluator then OR-combines them (either condition satisfies the requirement).
166
+
66
167
  ## Rule Collectors
67
168
 
68
169
  | Name | Derives permission/scope | Returns |
@@ -110,5 +211,6 @@ import { builtinCollectorsModule } from "@o3co/auth.policy-verifier.builtins";
110
211
 
111
212
  ## See Also
112
213
 
214
+ - [Extension guide (`docs/extending.md`)](../../docs/extending.md) — how to write custom `Rule` and `AttributeCollector` implementations; positioning of `builtins` as a basic set
113
215
  - [`@o3co/auth.policy-verifier.core`](../core/README.md) — core interfaces and attribute constants
114
216
  - [auth.policy-verifier root README](../../README.md) — full setup and configuration reference
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=AttrLiteralCompare.test.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttrLiteralCompare.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrLiteralCompare.test.mts"],"names":[],"mappings":""}
@@ -0,0 +1,253 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AttrLiteralCompare } from "#/rules/AttrLiteralCompare.mjs";
3
+ describe("AttrLiteralCompare", () => {
4
+ // ---------------------------------------------------------------------------
5
+ // Happy path: each op at boundary values
6
+ // ---------------------------------------------------------------------------
7
+ it("op=lt: returns true when attr < v (5 < 10)", () => {
8
+ const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: 10 });
9
+ const attrs = new Map([["score", 5]]);
10
+ expect(rule.verify(attrs)).toBe(true);
11
+ });
12
+ it("op=lt: returns false when attr === v (10 < 10 is false)", () => {
13
+ const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: 10 });
14
+ const attrs = new Map([["score", 10]]);
15
+ expect(rule.verify(attrs)).toBe(false);
16
+ });
17
+ it("op=lt: returns false when attr > v (15 < 10 is false)", () => {
18
+ const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: 10 });
19
+ const attrs = new Map([["score", 15]]);
20
+ expect(rule.verify(attrs)).toBe(false);
21
+ });
22
+ it("op=le: returns true when attr === v (10 <= 10)", () => {
23
+ const rule = new AttrLiteralCompare({ a: "score", op: "le", v: 10 });
24
+ const attrs = new Map([["score", 10]]);
25
+ expect(rule.verify(attrs)).toBe(true);
26
+ });
27
+ it("op=le: returns true when attr < v (5 <= 10)", () => {
28
+ const rule = new AttrLiteralCompare({ a: "score", op: "le", v: 10 });
29
+ const attrs = new Map([["score", 5]]);
30
+ expect(rule.verify(attrs)).toBe(true);
31
+ });
32
+ it("op=le: returns false when attr > v (15 <= 10 is false)", () => {
33
+ const rule = new AttrLiteralCompare({ a: "score", op: "le", v: 10 });
34
+ const attrs = new Map([["score", 15]]);
35
+ expect(rule.verify(attrs)).toBe(false);
36
+ });
37
+ it("op=gt: returns true when attr > v (15 > 10)", () => {
38
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
39
+ const attrs = new Map([["score", 15]]);
40
+ expect(rule.verify(attrs)).toBe(true);
41
+ });
42
+ it("op=gt: returns false when attr === v (10 > 10 is false)", () => {
43
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
44
+ const attrs = new Map([["score", 10]]);
45
+ expect(rule.verify(attrs)).toBe(false);
46
+ });
47
+ it("op=gt: returns false when attr < v (5 > 10 is false)", () => {
48
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
49
+ const attrs = new Map([["score", 5]]);
50
+ expect(rule.verify(attrs)).toBe(false);
51
+ });
52
+ it("op=ge: returns true when attr === v (10 >= 10)", () => {
53
+ const rule = new AttrLiteralCompare({ a: "score", op: "ge", v: 10 });
54
+ const attrs = new Map([["score", 10]]);
55
+ expect(rule.verify(attrs)).toBe(true);
56
+ });
57
+ it("op=ge: returns true when attr > v (15 >= 10)", () => {
58
+ const rule = new AttrLiteralCompare({ a: "score", op: "ge", v: 10 });
59
+ const attrs = new Map([["score", 15]]);
60
+ expect(rule.verify(attrs)).toBe(true);
61
+ });
62
+ it("op=ge: returns false when attr < v (5 >= 10 is false)", () => {
63
+ const rule = new AttrLiteralCompare({ a: "score", op: "ge", v: 10 });
64
+ const attrs = new Map([["score", 5]]);
65
+ expect(rule.verify(attrs)).toBe(false);
66
+ });
67
+ // ---------------------------------------------------------------------------
68
+ // Evaluation-time safe-deny: attr is not a number
69
+ // ---------------------------------------------------------------------------
70
+ it("returns false when attr is a string (not a number)", () => {
71
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 5 });
72
+ const attrs = new Map([["score", "10"]]);
73
+ expect(rule.verify(attrs)).toBe(false);
74
+ });
75
+ it("returns false when attr is a boolean (not a number)", () => {
76
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 0 });
77
+ const attrs = new Map([["score", true]]);
78
+ expect(rule.verify(attrs)).toBe(false);
79
+ });
80
+ // ---------------------------------------------------------------------------
81
+ // Evaluation-time: NaN attribute → false for all ops
82
+ // ---------------------------------------------------------------------------
83
+ it("op=lt: returns false when attr is NaN", () => {
84
+ const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: 10 });
85
+ const attrs = new Map([["score", NaN]]);
86
+ expect(rule.verify(attrs)).toBe(false);
87
+ });
88
+ it("op=le: returns false when attr is NaN", () => {
89
+ const rule = new AttrLiteralCompare({ a: "score", op: "le", v: 10 });
90
+ const attrs = new Map([["score", NaN]]);
91
+ expect(rule.verify(attrs)).toBe(false);
92
+ });
93
+ it("op=gt: returns false when attr is NaN", () => {
94
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
95
+ const attrs = new Map([["score", NaN]]);
96
+ expect(rule.verify(attrs)).toBe(false);
97
+ });
98
+ it("op=ge: returns false when attr is NaN", () => {
99
+ const rule = new AttrLiteralCompare({ a: "score", op: "ge", v: 10 });
100
+ const attrs = new Map([["score", NaN]]);
101
+ expect(rule.verify(attrs)).toBe(false);
102
+ });
103
+ // ---------------------------------------------------------------------------
104
+ // Evaluation-time: Infinity / -Infinity attributes
105
+ // ---------------------------------------------------------------------------
106
+ it("op=gt: Infinity > 100 returns true", () => {
107
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 100 });
108
+ const attrs = new Map([["score", Infinity]]);
109
+ expect(rule.verify(attrs)).toBe(true);
110
+ });
111
+ it("op=lt: -Infinity < 0 returns true", () => {
112
+ const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: 0 });
113
+ const attrs = new Map([["score", -Infinity]]);
114
+ expect(rule.verify(attrs)).toBe(true);
115
+ });
116
+ it("op=ge: Infinity >= Infinity returns true", () => {
117
+ const rule = new AttrLiteralCompare({ a: "score", op: "ge", v: Infinity });
118
+ const attrs = new Map([["score", Infinity]]);
119
+ expect(rule.verify(attrs)).toBe(true);
120
+ });
121
+ // ---------------------------------------------------------------------------
122
+ // Evaluation-time safe-deny: missing / null attr
123
+ // ---------------------------------------------------------------------------
124
+ it("returns false when the target attribute is missing", () => {
125
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 0 });
126
+ const attrs = new Map();
127
+ expect(rule.verify(attrs)).toBe(false);
128
+ });
129
+ it("returns false when the target attribute is null", () => {
130
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 0 });
131
+ const attrs = new Map([["score", null]]);
132
+ expect(rule.verify(attrs)).toBe(false);
133
+ });
134
+ // ---------------------------------------------------------------------------
135
+ // Construction errors: invalid 'a'
136
+ // ---------------------------------------------------------------------------
137
+ it("throws when 'a' is undefined", () => {
138
+ expect(() => new AttrLiteralCompare({ a: undefined, op: "gt", v: 0 })).toThrow(/AttrLiteralCompare.*'a'.*(non-empty string|got undefined)/);
139
+ });
140
+ it("throws when 'a' is an empty string", () => {
141
+ expect(() => new AttrLiteralCompare({ a: "", op: "gt", v: 0 })).toThrow(/AttrLiteralCompare.*'a'.*empty string/);
142
+ });
143
+ it("throws when 'a' is a non-string (number 42)", () => {
144
+ expect(() => new AttrLiteralCompare({ a: 42, op: "gt", v: 0 })).toThrow(/AttrLiteralCompare.*'a'.*got number/);
145
+ });
146
+ // ---------------------------------------------------------------------------
147
+ // Construction errors: invalid 'v'
148
+ // ---------------------------------------------------------------------------
149
+ it("throws when 'v' is missing (undefined)", () => {
150
+ expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: undefined })).toThrow(/AttrLiteralCompare.*'v'/);
151
+ });
152
+ it("throws when 'v' is null", () => {
153
+ expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: null })).toThrow(/AttrLiteralCompare.*'v'/);
154
+ });
155
+ it("throws when 'v' is a string", () => {
156
+ expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: "10" })).toThrow(/AttrLiteralCompare.*'v'/);
157
+ });
158
+ it("throws when 'v' is a boolean", () => {
159
+ expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: true })).toThrow(/AttrLiteralCompare.*'v'/);
160
+ });
161
+ it("throws when 'v' is a plain object", () => {
162
+ expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: {} })).toThrow(/AttrLiteralCompare.*'v'/);
163
+ });
164
+ it("throws when 'v' is NaN (fail-fast at construction)", () => {
165
+ expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: NaN })).toThrow(/AttrLiteralCompare.*'v'.*NaN/);
166
+ });
167
+ // ---------------------------------------------------------------------------
168
+ // Construction errors: invalid 'op'
169
+ // ---------------------------------------------------------------------------
170
+ it("throws when 'op' is missing (undefined)", () => {
171
+ expect(() => new AttrLiteralCompare({
172
+ a: "score",
173
+ op: undefined,
174
+ v: 0,
175
+ })).toThrow(/AttrLiteralCompare.*'op'/);
176
+ });
177
+ it("throws when 'op' is not a string (number)", () => {
178
+ expect(() => new AttrLiteralCompare({
179
+ a: "score",
180
+ op: 42,
181
+ v: 0,
182
+ })).toThrow(/AttrLiteralCompare.*'op'/);
183
+ });
184
+ it("throws when 'op' is an invalid string ('eq')", () => {
185
+ expect(() => new AttrLiteralCompare({
186
+ a: "score",
187
+ op: "eq",
188
+ v: 0,
189
+ })).toThrow(/AttrLiteralCompare.*'op'/);
190
+ });
191
+ // ---------------------------------------------------------------------------
192
+ // Construction errors: invalid 'group'
193
+ // ---------------------------------------------------------------------------
194
+ it("throws when 'group' is present but is an empty string", () => {
195
+ expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: 0, group: "" })).toThrow(/AttrLiteralCompare.*'group'.*empty string/);
196
+ });
197
+ it("throws when 'group' is present but is a non-string (number 42)", () => {
198
+ expect(() => new AttrLiteralCompare({
199
+ a: "score",
200
+ op: "gt",
201
+ v: 0,
202
+ group: 42,
203
+ })).toThrow(/AttrLiteralCompare.*'group'.*got number/);
204
+ });
205
+ // ---------------------------------------------------------------------------
206
+ // ruleType: default derived from a, op, v
207
+ // ---------------------------------------------------------------------------
208
+ it("derives default ruleType from a, op, and v", () => {
209
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
210
+ expect(rule.ruleType).toBe("attr_literal_compare:score:gt:10");
211
+ });
212
+ it("derives default ruleType for negative v", () => {
213
+ const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: -5 });
214
+ expect(rule.ruleType).toBe("attr_literal_compare:score:lt:-5");
215
+ });
216
+ it("derives default ruleType for Infinity v", () => {
217
+ const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: Infinity });
218
+ expect(rule.ruleType).toBe("attr_literal_compare:score:lt:Infinity");
219
+ });
220
+ it("distinct op or v produce distinct ruleTypes (AND semantics)", () => {
221
+ const r1 = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
222
+ const r2 = new AttrLiteralCompare({ a: "score", op: "ge", v: 10 });
223
+ const r3 = new AttrLiteralCompare({ a: "score", op: "gt", v: 20 });
224
+ expect(r1.ruleType).not.toBe(r2.ruleType);
225
+ expect(r1.ruleType).not.toBe(r3.ruleType);
226
+ });
227
+ // ---------------------------------------------------------------------------
228
+ // ruleType: group override
229
+ // ---------------------------------------------------------------------------
230
+ it("uses the provided group string as ruleType, overriding the default", () => {
231
+ const r1 = new AttrLiteralCompare({ a: "score", op: "gt", v: 10, group: "score-check" });
232
+ const r2 = new AttrLiteralCompare({ a: "score", op: "ge", v: 11, group: "score-check" });
233
+ expect(r1.ruleType).toBe("score-check");
234
+ expect(r2.ruleType).toBe("score-check");
235
+ });
236
+ // ---------------------------------------------------------------------------
237
+ // code and message fields
238
+ // ---------------------------------------------------------------------------
239
+ it("has code 'attr_compare_violated'", () => {
240
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
241
+ expect(rule.code).toBe("attr_compare_violated");
242
+ });
243
+ it("message contains the attribute name, op, and v", () => {
244
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
245
+ expect(rule.message).toContain("score");
246
+ expect(rule.message).toContain("gt");
247
+ expect(rule.message).toContain("10");
248
+ });
249
+ it("message starts with a capital letter", () => {
250
+ const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
251
+ expect(rule.message[0]).toBe(rule.message[0]?.toUpperCase());
252
+ });
253
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=AttrLiteralEqual.test.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttrLiteralEqual.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrLiteralEqual.test.mts"],"names":[],"mappings":""}