@o3co/auth.policy-verifier.builtins 0.14.0 → 0.16.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 (85) hide show
  1. package/README.ja.md +45 -7
  2. package/README.md +47 -8
  3. package/dist/collectors/DelegationRangeCollector.d.mts +29 -0
  4. package/dist/collectors/DelegationRangeCollector.d.mts.map +1 -0
  5. package/dist/collectors/DelegationRangeCollector.mjs +29 -0
  6. package/dist/collectors/PayloadClaimAttributeCollector.d.mts +24 -39
  7. package/dist/collectors/PayloadClaimAttributeCollector.d.mts.map +1 -1
  8. package/dist/collectors/PayloadClaimAttributeCollector.mjs +26 -41
  9. package/dist/collectors/PayloadScopeCollector.d.mts +1 -1
  10. package/dist/collectors/PayloadSubjectIdCollector.d.mts +1 -1
  11. package/dist/collectors/PayloadSubjectIdCollector.mjs +1 -1
  12. package/dist/collectors/RequestContextAttributeCollector.d.mts +24 -62
  13. package/dist/collectors/RequestContextAttributeCollector.d.mts.map +1 -1
  14. package/dist/collectors/RequestContextAttributeCollector.mjs +33 -74
  15. package/dist/collectors/StaticPermissionCollector.d.mts +7 -9
  16. package/dist/collectors/StaticPermissionCollector.d.mts.map +1 -1
  17. package/dist/collectors/StaticPermissionCollector.mjs +7 -9
  18. package/dist/collectors/StaticRoleCollector.d.mts +9 -10
  19. package/dist/collectors/StaticRoleCollector.d.mts.map +1 -1
  20. package/dist/collectors/StaticRoleCollector.mjs +10 -11
  21. package/dist/collectors/_attributeMapping.d.mts +1 -0
  22. package/dist/collectors/_attributeMapping.d.mts.map +1 -1
  23. package/dist/collectors/_attributeMapping.mjs +3 -0
  24. package/dist/collectors/_claims.d.mts +2 -3
  25. package/dist/collectors/_claims.d.mts.map +1 -1
  26. package/dist/collectors/_claims.mjs +3 -4
  27. package/dist/delegation/range.d.mts +48 -0
  28. package/dist/delegation/range.d.mts.map +1 -0
  29. package/dist/delegation/range.mjs +108 -0
  30. package/dist/index.d.mts +5 -0
  31. package/dist/index.d.mts.map +1 -1
  32. package/dist/index.mjs +7 -0
  33. package/dist/keys.d.mts +9 -0
  34. package/dist/keys.d.mts.map +1 -0
  35. package/dist/keys.mjs +31 -0
  36. package/dist/module.d.mts.map +1 -1
  37. package/dist/module.mjs +4 -0
  38. package/dist/resource/DotNotationResourceParser.d.mts +16 -19
  39. package/dist/resource/DotNotationResourceParser.d.mts.map +1 -1
  40. package/dist/resource/DotNotationResourceParser.mjs +20 -25
  41. package/dist/rules/AttrLiteralCompare.d.mts +3 -5
  42. package/dist/rules/AttrLiteralCompare.d.mts.map +1 -1
  43. package/dist/rules/AttrLiteralCompare.mjs +3 -5
  44. package/dist/rules/AttrLiteralEqual.d.mts +3 -5
  45. package/dist/rules/AttrLiteralEqual.d.mts.map +1 -1
  46. package/dist/rules/AttrLiteralEqual.mjs +3 -5
  47. package/dist/rules/AttrLiteralIn.d.mts +3 -5
  48. package/dist/rules/AttrLiteralIn.d.mts.map +1 -1
  49. package/dist/rules/AttrLiteralIn.mjs +3 -5
  50. package/dist/rules/AttrLiteralNotEqual.d.mts +6 -8
  51. package/dist/rules/AttrLiteralNotEqual.d.mts.map +1 -1
  52. package/dist/rules/AttrLiteralNotEqual.mjs +6 -8
  53. package/dist/rules/AttrLiteralNotIn.d.mts +4 -6
  54. package/dist/rules/AttrLiteralNotIn.d.mts.map +1 -1
  55. package/dist/rules/AttrLiteralNotIn.mjs +4 -6
  56. package/dist/rules/AttrMatchRule.d.mts +1 -1
  57. package/dist/rules/AttrMatchRule.mjs +1 -1
  58. package/dist/rules/AttrPairCompare.d.mts +3 -5
  59. package/dist/rules/AttrPairCompare.d.mts.map +1 -1
  60. package/dist/rules/AttrPairCompare.mjs +3 -5
  61. package/dist/rules/AttrPairEqual.d.mts +6 -9
  62. package/dist/rules/AttrPairEqual.d.mts.map +1 -1
  63. package/dist/rules/AttrPairEqual.mjs +6 -9
  64. package/dist/rules/AttrPairNotEqual.d.mts +3 -5
  65. package/dist/rules/AttrPairNotEqual.d.mts.map +1 -1
  66. package/dist/rules/AttrPairNotEqual.mjs +3 -5
  67. package/dist/rules/HasPermission.d.mts +9 -10
  68. package/dist/rules/HasPermission.d.mts.map +1 -1
  69. package/dist/rules/HasPermission.mjs +16 -20
  70. package/dist/rules/HasScope.d.mts +11 -14
  71. package/dist/rules/HasScope.d.mts.map +1 -1
  72. package/dist/rules/HasScope.mjs +8 -11
  73. package/dist/rules/WithinDelegationRange.d.mts +22 -0
  74. package/dist/rules/WithinDelegationRange.d.mts.map +1 -0
  75. package/dist/rules/WithinDelegationRange.mjs +38 -0
  76. package/dist/rules/_sharedValidation.d.mts +12 -19
  77. package/dist/rules/_sharedValidation.d.mts.map +1 -1
  78. package/dist/rules/_sharedValidation.mjs +17 -25
  79. package/dist/rules/collectors/DelegationRangeRuleCollector.d.mts +36 -0
  80. package/dist/rules/collectors/DelegationRangeRuleCollector.d.mts.map +1 -0
  81. package/dist/rules/collectors/DelegationRangeRuleCollector.mjs +36 -0
  82. package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts +19 -28
  83. package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts.map +1 -1
  84. package/dist/rules/collectors/ResourceActionScopeRuleCollector.mjs +21 -30
  85. package/package.json +5 -5
package/README.ja.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @o3co/auth.policy-verifier.builtins
2
2
 
3
- 最終更新: 2026-09-24
3
+ 最終更新: 2026-10-01
4
4
 
5
5
  auth.policy-verifier 向けの組み込み attribute collector、rule collector、および resource parser です。
6
6
 
@@ -10,7 +10,7 @@ auth.policy-verifier 向けの組み込み attribute collector、rule collector
10
10
 
11
11
  core のインターフェースを実装した、小さな既製のセットです: attribute collector、rule、rule collector、resource parser、そしてそれらを server の `createApp` などのホストに登録する `builtinCollectorsModule`。依存は `@o3co/auth.policy-verifier.core` だけです。`templates/standalone` がこれを組み込み、`server` はテストでのみ使います(`devDependencies`)。
12
12
 
13
- - **所有するもの:** これらの実装が何を読み、何を書き、どう照合するか — JWT クレーム(`sub`、`azp`、`scope`、宣言したクレーム)から属性キーへの対応(#170)、`HasScope` / `HasPermission` と比較ルールの照合規則、ドット記法のリソース文法。
13
+ - **所有するもの:** これらの実装が何を読み、何を書き、どう照合するか — JWT クレーム(`sub`、`azp`、`scope`、宣言したクレーム)から属性キーへの対応(#170)、`HasScope` / `HasPermission` と比較ルールの照合規則、ドット記法のリソース文法、クレーム契約の委任 range の文法と包含ルールの実装(契約そのものは umbrella リポジトリのもの)。
14
14
  - **所有しないもの:** パイプライン、グルーピング、判定(core)、認証や HTTP(`server`)、ポリシーエンジンや `AsyncRule`(`cedar`)、I/O を行う collector(利用側が書くもの。[docs/extending.ja.md](../../docs/extending.ja.md))。
15
15
  - **core と別パッケージである理由:** core はエンジン中立の契約で、subject のフィールドを一切名指ししません(#170)。クレームの語彙と具体的な照合はその線の反対側、つまりここに置きます([AGENTS.md — Core Vocabulary Scope](../../AGENTS.md#core-vocabulary-scope))。また任意導入でもあります: builtins は意図的に基本セットであってカタログではない([docs/extending.ja.md](../../docs/extending.ja.md))ので、collector と rule を自前で書くデプロイメントはインストール不要で、core もこれに合わせて肥大化しません。
16
16
 
@@ -36,6 +36,7 @@ npm install @o3co/auth.policy-verifier.builtins
36
36
  | `StaticRoleCollector` | — | `ATTR_ROLES: Role[]` | `{ roles: Role[] }` |
37
37
  | `RequestContextAttributeCollector` | `requestContext` の宣言済みフィールド | 運用者が決めたキー | `{ attributes: Mapping[] }` |
38
38
  | `PayloadClaimAttributeCollector` | 検証済み `subject` の宣言済みクレーム | 運用者が決めたキー、または core の 5 キー | `{ attributes: Mapping[] }` (#219) |
39
+ | `DelegationRangeCollector` | トークンの `authorization_details` のうち、設定した type のエントリのパス | `ATTR_DELEGATION_RANGE: string[]` | `{ type: string, claim?: string }` — [委任トークン](#委任トークン-range-と-actor) |
39
40
 
40
41
  `StaticPermissionCollector` と `StaticRoleCollector` は、リクエストのコンテキストに関わらず、コンストラクタに渡した値を常に出力します。渡された値は構築時にコピーされます — 配列、そして `StaticRoleCollector` では各 `Role` とその `permissions` も — ので、構築後に config を変更しても出力は変わりません(#255)。`permissions` と `roles` は配列でなければなりません。それ以外の値は、文字列も含めて(`["posts.*"]` のつもりで `permissions = "posts.*"` と書いた場合など)構築時に `TypeError` で拒否され、デプロイは起動時に失敗します(#264)。
41
42
 
@@ -52,7 +53,7 @@ npm install @o3co/auth.policy-verifier.builtins
52
53
  ] }
53
54
  ```
54
55
 
55
- マッピングの形は `RequestContextAttributeCollector` と同じです(`{ from, to?, type? }`、完全一致するキーが dot path より優先、own property のみ)。違うのは読み取り元であり、したがって信頼度です: subject バッグは authenticator が検証したものなので、request-context コレクターが拒否する core の 5 キー — `scopes`、`permissions`、`roles`、`userId`、`clientId` — にもマッピングを着地させて**かまいません**。2 つのコレクターが 1 つのリストキーに書けば union されます。それはデプロイが issuer 由来の 2 つのソースを合成しているということであり、config にそう書かれています。一方、2 つのコレクターが異なる値で書いた*スカラー*キーは `AttributeConflictError` を投げ、すべてのリクエストを deny します — `PayloadSubjectIdCollector` も `userId` / `clientId` を書いている間は、そこへマッピングしないでください。別のパッケージが予約したキー(cedar の `request*`)は引き続き拒否されます。それらは subject ではなくリクエストから導出されるものだからです。そして、マッピングするのは IdP が自身の登録データや管理データから埋めるクレームだけにしてください: ユーザーが編集できるメタデータ(Clerk の `unsafe_metadata`、Auth0 の `user_metadata`)から発行されたクレームは、署名されてはいても信頼できるものではありません。
56
+ マッピングの形は `RequestContextAttributeCollector` と同じです(`{ from, to?, type? }`、完全一致するキーが dot path より優先、own property のみ)。違うのは読み取り元であり、したがって信頼度です: subject バッグは authenticator が検証したものなので、request-context コレクターが拒否する core の 5 キー — `scopes`、`permissions`、`roles`、`userId`、`clientId` — にもマッピングを着地させて**かまいません**。2 つのコレクターが 1 つのリストキーに書けば union されます。それはデプロイが issuer 由来の 2 つのソースを合成しているということであり、config にそう書かれています。一方、2 つのコレクターが異なる値で書いた*スカラー*キーは `AttributeConflictError` を投げ、すべてのリクエストを deny します — `PayloadSubjectIdCollector` も `userId` / `clientId` を書いている間は、そこへマッピングしないでください。別のパッケージが予約したキーは引き続き拒否されます。そのパッケージが自分の collector で書くキーだからです(cedar の `request*` はリクエストから、このパッケージの `delegationRange` は `DelegationRangeCollector` が絞り込むクレームから)。クレームをそこへマッピングすると、2 つ目の書き手になります。そして、マッピングするのは IdP が自身の登録データや管理データから埋めるクレームだけにしてください: ユーザーが編集できるメタデータ(Clerk の `unsafe_metadata`、Auth0 の `user_metadata`)から発行されたクレームは、署名されてはいても信頼できるものではありません。
56
57
 
57
58
  scope クレームについては `PayloadScopeCollector { claim = "scp" }` を優先してください。こちらはスペース区切り文字列の形も読み、`ResourceActionScopeRuleCollector { claim = "scp" }` と組にすることで、両者が同じ claim を見ます(それぞれが自分の `claim` を持ち、一致は検査されないので両方に設定してください)。rule collector は claim の有無だけを見ます — 使える scope リストを持たない claim(`""`、数値)は scope を生まない一方、`scopeless = "skip"` では scope ありとして扱われます。
58
59
 
@@ -89,7 +90,7 @@ scope クレームについては `PayloadScopeCollector { claim = "scp" }` を
89
90
  { from = "scopes", to = "requestedScopes", type = "string[]" }
90
91
  ```
91
92
 
92
- 予約集合は**このパッケージが保持するリストではありません**。core のレジストリ(`RESERVED_ATTRIBUTE_KEYS` / `reserveAttributeKeys` / `attributeKeyReservation`)です。core は自身の 5 キー — `scopes`、`permissions`、`roles`、`userId`、`clientId` — を予約し、属性語彙を持つ各パッケージはモジュールロード時に自分の語彙を予約します。`@o3co/auth.policy-verifier.cedar` は `requestAction` / `requestResourceType` / `requestResourceId` / `requestResourceRaw` を予約します。プロジェクト側の Collector も、書き込むキーを同じ方法で予約してください。設定でそのパッケージの Collector 名を書ける構成は、すでにそのパッケージを import しているため、このコレクターが構築される前にキーは登録済みです — core から見えない語彙まで guard が効くのはこのためです。拒否メッセージは所有パッケージ名を示し、それ自体が予約されていない代替名を提案します。
93
+ 予約集合は**このパッケージが保持するリストではありません**。core のレジストリ(`RESERVED_ATTRIBUTE_KEYS` / `reserveAttributeKeys` / `attributeKeyReservation`)です。core は自身の 5 キー — `scopes`、`permissions`、`roles`、`userId`、`clientId` — を予約し、属性語彙を持つ各パッケージはモジュールロード時に自分の語彙を予約します。`@o3co/auth.policy-verifier.cedar` は `requestAction` / `requestResourceType` / `requestResourceId` / `requestResourceRaw` を予約し、このパッケージは `delegationRange`(`ATTR_DELEGATION_RANGE`)を予約します。プロジェクト側の Collector も、書き込むキーを同じ方法で予約してください。設定でそのパッケージの Collector 名を書ける構成は、すでにそのパッケージを import しているため、このコレクターが構築される前にキーは登録済みです — core から見えない語彙まで guard が効くのはこのためです。拒否メッセージは所有パッケージ名を示し、それ自体が予約されていない代替名を提案します。
93
94
 
94
95
  `context` は呼び出し側のもので、これらのキーはデプロイ側のものです。既定のサーバーでは `scopes` / `userId` / `clientId` は**署名検証済みトークン**から読まれ、`permissions` / `roles` は builtin の Rule が判断に使う権限を、cedar の 4 キーはパース済みリクエストを運びます。信頼レベルがまったく異なり、リクエストボディがそれらと同じバケツに合流してはなりません。
95
96
 
@@ -97,6 +98,35 @@ scope クレームについては `PayloadScopeCollector { claim = "scp" }` を
97
98
 
98
99
  スカラーキーなら安全というわけでもなく、理由は 2 つあります。双方が書けば値が食い違い、`AttributeConflictError` でそのリクエストは拒否されます — fail-closed ではありますが、起動時の拒否ではなく予告のない拒否です。そして所有側のコレクターが*条件付きでしか*書かないキーには、そもそも競合相手がいません。cedar の `RequestFactsCollector` は `"document"` のような id を持たないリソースでは `requestResourceId` を書かないため、`{ from = "rid", to = "requestResourceId" }` は誰とも競合せずに着地し、Cedar の resource エンティティが呼び出し側のリクエストボディから組み立てられてしまいます。
99
100
 
101
+ ### 委任トークン: range と actor
102
+
103
+ 委任 grant が発行したトークン(subject のために client が動くもの)は、その range を 1 種類の type の RFC 9396 `authorization_details` エントリ(各 `{ "type": <その type>, "path": "<パス>" }`)として、動いている client を `act.sub`(RFC 8693 §4.1)として持ちます。パスの文法と包含ルールは、provider–verifier の[クレーム契約](https://github.com/o3co/auth/blob/develop/docs/claims-contract.md)のものです:
104
+
105
+ - パスは `(type(:id)?.)*action`。`type` と `action` は `[a-z][a-z0-9_]*`、`id` は `[A-Za-z0-9_~-]` で、それ以外の文字は大文字の 16 進でパーセントエンコードします。id は書かれたとおりに比較されるので、`%41` と `A` は別の id です。issuer とリソースは同じようにエンコードしてください。
106
+ - エントリがパスの先頭からセグメント単位の接頭辞になっていれば、そのエントリはパスを含みます。id を持たないエントリのセグメントは、同じ type の任意の id を含みます。`project:p1.report` は `project:p1.report:r7.run` を含みます。エントリの最後の要素は、action を名指すときも type として比較されるので、`project:p1.run` は `project:p1.run.more` を含みます。action の名前と type の名前は分けてください。
107
+
108
+ range をポリシーと合わせて判定するには、1 か所から読んだ同じ `type` で 2 つを設定します:
109
+
110
+ ```hocon
111
+ attribute { collectors = [
112
+ { collector = "PayloadScopeCollector" }
113
+ # issuer の委任 type。未設定なら両方のエントリに `type` がなく、起動が拒否される
114
+ { collector = "DelegationRangeCollector", type = ${?DELEGATION_TYPE} }
115
+ # 動いている client。ポリシーから参照する場合
116
+ { collector = "PayloadClaimAttributeCollector", attributes = [ { from = "act.sub", to = "actorId" } ] }
117
+ ] }
118
+ rule { collectors = [
119
+ { collector = "ResourceActionScopeRuleCollector" }
120
+ { collector = "DelegationRangeRuleCollector", type = ${?DELEGATION_TYPE} }
121
+ ] }
122
+ ```
123
+
124
+ - `DelegationRangeCollector` はエントリのパスを `ATTR_DELEGATION_RANGE`(`"delegationRange"`、このパッケージが予約)に書きます。文法外のパスは除外され、range が狭まる方向に働きます。その type のエントリはあるが読めるパスがないトークンは空の range になり、何も含みません。クレームが別の形(リストでない、またはエントリのオブジェクト以外を含むリスト)で存在するトークンも、エントリがどの type を名指すかにかかわらず同じです。`authorization_details` をほかの RFC 9396 の type と共有するトークンは、エントリのオブジェクトのリストとして持たないと、すべて deny されます。
125
+ - `DelegationRangeRuleCollector` は、トークンが range を持つとき(その type のエントリがあるか、クレームが別の形で存在するとき)、リクエストのパス(`<resource.raw>.<action>`。`resource: "project:p1.report:r7"` と `action: "run"` なら `project:p1.report:r7.run`)について [`WithinDelegationRange`](#withindelegationrange) ルールを 1 つ生成し、持たないときは何も生成しません。action は文法上の 1 つの action でなければなりません。複数の要素からなる action(`report.delete`)は結合したパスの区切りを変えてしまい、親へのリクエストが range に含まれる子へのリクエストとして読めてしまうので、その場合ルールは fail します。このルールは独立したグループで、絞るルール(`restricts: true`)なので、委任トークンは range とポリシーの両方が許すものだけを許されます。ポリシーのルールが 1 つも適用されない委任トークンは、range だけで判定されることはなく、`rule.onEmptyRuleSet`(既定では `no_applicable_rule`)で判定されます。range のないトークンは、この 2 つがない場合と同じく判定されます。
126
+ - 2 つには同じ `type` と `claim`(既定 `authorization_details`)を与えてください。一致しているかを確かめる仕組みはありません。食い違うと、2 通りに壊れます。ルール collector がトークンのエントリを見つけ、属性 collector が見つけない場合は、ルールが range を見つけられず、リクエストは deny されます。逆の場合はルールが生成されず、**range は適用されません**。
127
+ - リクエストのパスは、設定したリソースパーサーにかかわらず、呼び出し元が送ったリソース文字列から作られます。別の文法で書かれたリソース(`projects/p1/reports/r7`)はパスとして読めず、委任されたリクエストはすべて deny されます。
128
+ - リクエストが名指すリソースは、親の連鎖も含めて呼び出し元の主張です。連鎖に依拠するポリシー(包含、`in`)は、その連鎖を保存しているストアを読む独自の collector で、使う前に確かめる必要があります([docs/extending.md](../../docs/extending.md#writing-a-custom-attributecollector))。
129
+
100
130
  ## Rules
101
131
 
102
132
  ### HasPermission
@@ -141,6 +171,10 @@ new AttrMatchRule({ a: string, b: string, group?: string })
141
171
  - 純粋な述語です。`CollectorContext` を参照しません。比較対象の値はプロジェクト側の上流 `AttributeCollector` が attrs に格納し、プロジェクト側の `RuleCollector` でこの Rule を構築します。
142
172
  - `ruleType` の既定値は `"attr_match:${a}:${b}"` です。評価器は `ruleType` 内で OR、`ruleType` 間で AND を取るので、この既定値により異なる2つの比較は AND(両方必要)として扱われます。2つの比較を OR 結合したい場合(例「DID または email で一致」)は、両方の Rule に同じ `group` を指定してください。その `group` 値が `ruleType` として使われます。
143
173
 
174
+ ### WithinDelegationRange
175
+
176
+ `ATTR_DELEGATION_RANGE` のエントリのどれかが、クレーム契約の包含ルール([委任トークン](#委任トークン-range-と-actor))でリクエストのパスを含むとき pass します。range がない場合や、パスを含む文字列が 1 つもない場合は fail し、文法外のリクエストパスも fail します。`ruleType` は `delegation_range`、`code` は `outside_delegation_range` です。絞るルールなので、ほかのルールが許すものを絞るだけで、それ自体は許可の根拠になりません。`DelegationRangeRuleCollector` が生成します。別の包含ルールが必要なデプロイは、`ATTR_DELEGATION_RANGE` を読む独自のルールと、それを生成するルール collector を書き、`DelegationRangeRuleCollector` の代わりに使います。
177
+
144
178
  ## Attribute Comparison Rules
145
179
 
146
180
  属性比較 Rule 群は 2 軸のマトリクスで構成されます: **family**(Literal または Pair)と **operator**(Equal、NotEqual、In、NotIn、Compare)。
@@ -248,13 +282,15 @@ new AttrPairCompare({ a: string, op: "lt" | "le" | "gt" | "ge", b: string, group
248
282
  | --- | --- | --- |
249
283
  | `ResourceActionPermissionRuleCollector` | `"<resource.raw>.perm:<action>"` | `[HasPermission(...)]` |
250
284
  | `ResourceActionScopeRuleCollector` | `"<action>:<resource.resourceType>"` | `[HasScope(...)]` |
285
+ | `DelegationRangeRuleCollector` | `"<resource.raw>.<action>"`(range を持つトークンのとき) | `[WithinDelegationRange(...)]` または `[]` |
251
286
 
252
- `ResourceActionPermissionRuleCollector` にコンストラクタ引数はありません。
287
+ `ResourceActionPermissionRuleCollector` にコンストラクタ引数はありません。`DelegationRangeRuleCollector` は `{ type: string, claim?: string }` を受け取ります — [委任トークン](#委任トークン-range-と-actor)。
253
288
  `ResourceActionScopeRuleCollector` は `{ scopeless?: "deny" | "skip", allowBareScopeRewrite?: boolean, claim?: string }` を受け取ります — `claim` はトークンが scope を主張していることを示す claim の名前です(既定 `scope`。`PayloadScopeCollector` が読むもの、例えば `scp` に合わせる)。
254
289
 
255
290
  - `scopeless`(既定 `"deny"`): 既定ではリクエストごとに必ず `HasScope` ルールを生成するため、scope claim(`scope`、または `claim` が指す claim)を持たない
256
- トークンはこのルールに落ちます。`"skip"` は scopeless トークンに対してルールを生成しませんが、ルールが 1 つも集まらない
257
- リクエストは deny されるため、別のルールグループが認可を担うパイプラインでのみ使ってください。
291
+ トークンはこのルールに落ちます。`"skip"` は scopeless トークンに対してルールを生成しませんが、許可するルールが 1 つも集まらない
292
+ リクエストは deny されるため、許可する別のルールグループが認可を担うパイプラインでのみ使ってください。
293
+ [`WithinDelegationRange`](#withindelegationrange) のような絞るルールは認可になりません。
258
294
  - `allowBareScopeRewrite`(既定 `false`): [`HasScope`](#hasscope) へそのまま渡されます。issuer が `{action}:{resourceType}`
259
295
  形式(`read:project`)ではなくプレフィックスなしのリソース名(`project`)を発行する場合にのみ `true` にしてください。
260
296
 
@@ -327,8 +363,10 @@ import { builtinCollectorsModule } from "@o3co/auth.policy-verifier.builtins";
327
363
  | `attributeCollector` | `"StaticRoleCollector"` | `(config) => new StaticRoleCollector(config)` |
328
364
  | `attributeCollector` | `"RequestContextAttributeCollector"` | `(config) => new RequestContextAttributeCollector(config)` |
329
365
  | `attributeCollector` | `"PayloadClaimAttributeCollector"` | `(config) => new PayloadClaimAttributeCollector(config)` |
366
+ | `attributeCollector` | `"DelegationRangeCollector"` | `(config) => new DelegationRangeCollector(config)` |
330
367
  | `ruleCollector` | `"ResourceActionScopeRuleCollector"` | `(config) => new ResourceActionScopeRuleCollector(config)` |
331
368
  | `ruleCollector` | `"ResourceActionPermissionRuleCollector"` | `() => new ResourceActionPermissionRuleCollector()` |
369
+ | `ruleCollector` | `"DelegationRangeRuleCollector"` | `(config) => new DelegationRangeRuleCollector(config)` |
332
370
  | `resourceParser` | `"DotNotationResourceParser"` | `() => new DotNotationResourceParser()` |
333
371
 
334
372
  ## 関連
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @o3co/auth.policy-verifier.builtins
2
2
 
3
- Last updated: 2026-09-24
3
+ Last updated: 2026-10-01
4
4
 
5
5
  Built-in attribute collectors, rule collectors, and resource parser for auth.policy-verifier.
6
6
 
@@ -16,8 +16,9 @@ a host such as the server's `createApp`. It depends on `@o3co/auth.policy-verifi
16
16
 
17
17
  - **Owns** what these implementations read, write and match: the mapping from JWT claims
18
18
  (`sub`, `azp`, `scope`, declared claims) to attribute keys (#170), the matching rules of
19
- `HasScope` / `HasPermission` and the comparison rules, and the dot-notation resource
20
- grammar.
19
+ `HasScope` / `HasPermission` and the comparison rules, the dot-notation resource
20
+ grammar, and the implementation of the claims contract's delegation-range grammar and
21
+ containment rule (the contract itself is the umbrella repository's).
21
22
  - **Does not own** the pipelines, the grouping or the decision (core), authentication or HTTP
22
23
  (`server`), policy engines or `AsyncRule`s (`cedar`), or collectors that do I/O — those are
23
24
  the consumer's to write ([docs/extending.md](../../docs/extending.md)).
@@ -51,6 +52,7 @@ All collectors implement `AttributeCollector`.
51
52
  | `StaticRoleCollector` | — | `ATTR_ROLES: Role[]` | `{ roles: Role[] }` |
52
53
  | `RequestContextAttributeCollector` | declared fields of `requestContext` | the operator's own keys | `{ attributes: Mapping[] }` |
53
54
  | `PayloadClaimAttributeCollector` | declared claims of the verified `subject` | the operator's own keys, or core's five | `{ attributes: Mapping[] }` (#219) |
55
+ | `DelegationRangeCollector` | the paths of the token's `authorization_details` entries of the configured type | `ATTR_DELEGATION_RANGE: string[]` | `{ type: string, claim?: string }` — [Delegated tokens](#delegated-tokens-range-and-actor) |
54
56
 
55
57
  `StaticPermissionCollector` and `StaticRoleCollector` always emit the values supplied at construction time, regardless of request context. They copy what they were given at construction — the array, and for `StaticRoleCollector` each `Role` and its `permissions` — so mutating the config afterwards changes nothing they emit (#255). `permissions` and `roles` must be arrays: anything else, a string included (`permissions = "posts.*"` where `["posts.*"]` was meant), is refused with a `TypeError` at construction, so the deployment fails at boot (#264).
56
58
 
@@ -67,7 +69,7 @@ Promotes declared claims of the **verified subject** into attributes (#219) —
67
69
  ] }
68
70
  ```
69
71
 
70
- Same mapping shape as `RequestContextAttributeCollector` (`{ from, to?, type? }`, an exact key winning over a dot path, own properties only). What differs is the source, and therefore the trust: the subject bag is what the authenticator verified, so a mapping **may** land on core's five keys — `scopes`, `permissions`, `roles`, `userId`, `clientId` — where the request-context collector refuses them. Two collectors writing one list key union it; that is the deployment composing two issuer-derived sources, and it says so in config. A *scalar* key written by two collectors with different values throws `AttributeConflictError` and denies every request — do not map onto `userId` / `clientId` while `PayloadSubjectIdCollector` also writes them. Keys another package reserved (cedar's `request*`) stay refused, because they are derived from the request, not from the subject. And map only claims the IdP populates from its own registration or admin data: a claim minted from user-editable metadata (Clerk's `unsafe_metadata`, Auth0's `user_metadata`) is signed, not trusted.
72
+ Same mapping shape as `RequestContextAttributeCollector` (`{ from, to?, type? }`, an exact key winning over a dot path, own properties only). What differs is the source, and therefore the trust: the subject bag is what the authenticator verified, so a mapping **may** land on core's five keys — `scopes`, `permissions`, `roles`, `userId`, `clientId` — where the request-context collector refuses them. Two collectors writing one list key union it; that is the deployment composing two issuer-derived sources, and it says so in config. A *scalar* key written by two collectors with different values throws `AttributeConflictError` and denies every request — do not map onto `userId` / `clientId` while `PayloadSubjectIdCollector` also writes them. Keys another package reserved stay refused, because that package writes them with a collector of its own — cedar's `request*` from the request, this package's `delegationRange` from the claim `DelegationRangeCollector` narrows — and a claim mapped onto one would be a second writer. And map only claims the IdP populates from its own registration or admin data: a claim minted from user-editable metadata (Clerk's `unsafe_metadata`, Auth0's `user_metadata`) is signed, not trusted.
71
73
 
72
74
  For the scope claim specifically, prefer `PayloadScopeCollector { claim = "scp" }`, which also reads the space-delimited string form and pairs with `ResourceActionScopeRuleCollector { claim = "scp" }` — set on both, since each keeps its own `claim` and nothing checks they match — so both look at the same claim. The rule collector checks only that the claim is present: a claim holding no usable scope list (`""`, a number) yields no scopes but still counts as scoped under `scopeless = "skip"`.
73
75
 
@@ -104,7 +106,7 @@ A mapping's `to` may not name a **reserved attribute key**. Naming one is a **co
104
106
  { from = "scopes", to = "requestedScopes", type = "string[]" }
105
107
  ```
106
108
 
107
- The reserved set is **not a list this package keeps**. It is core's registry (`RESERVED_ATTRIBUTE_KEYS`, `reserveAttributeKeys`, `attributeKeyReservation`): core reserves its own five — `scopes`, `permissions`, `roles`, `userId`, `clientId` — and every package that owns attribute vocabulary reserves its own at module load. `@o3co/auth.policy-verifier.cedar` reserves `requestAction`, `requestResourceType`, `requestResourceId` and `requestResourceRaw`; a project-side collector should reserve the keys it writes the same way. A composition that can name a package's collectors in config has already imported that package, so its keys are registered before this collector is constructed — which is why the guard covers vocabulary core cannot see. The refusal names the owning package, and suggests a rename that is not itself reserved.
109
+ The reserved set is **not a list this package keeps**. It is core's registry (`RESERVED_ATTRIBUTE_KEYS`, `reserveAttributeKeys`, `attributeKeyReservation`): core reserves its own five — `scopes`, `permissions`, `roles`, `userId`, `clientId` — and every package that owns attribute vocabulary reserves its own at module load. `@o3co/auth.policy-verifier.cedar` reserves `requestAction`, `requestResourceType`, `requestResourceId` and `requestResourceRaw`, and this package reserves `delegationRange` (`ATTR_DELEGATION_RANGE`); a project-side collector should reserve the keys it writes the same way. A composition that can name a package's collectors in config has already imported that package, so its keys are registered before this collector is constructed — which is why the guard covers vocabulary core cannot see. The refusal names the owning package, and suggests a rename that is not itself reserved.
108
110
 
109
111
  `context` is the caller's, and those keys are the deployment's. Under the default server `scopes`, `userId` and `clientId` are read out of the **signature-verified token**, `permissions` / `roles` carry the entitlements the builtin rules decide from, and cedar's four carry the parsed request — so the two sides of such a mapping carry entirely different trust, and the request body must not join them in one bucket.
110
112
 
@@ -112,6 +114,35 @@ What makes it worth refusing rather than documenting is the merge: `AttributePip
112
114
 
113
115
  A scalar key is no safer, in two ways. Where both sides write it the values disagree and `AttributeConflictError` denies the request — fail-closed, but an unannounced denial rather than a refusal at boot. And where the owning collector writes its key only *sometimes* there is no second writer at all: cedar's `RequestFactsCollector` omits `requestResourceId` for an id-less resource such as `"document"`, so `{ from = "rid", to = "requestResourceId" }` would land unopposed and the Cedar resource entity would be built from the caller's own request body.
114
116
 
117
+ ### Delegated tokens: range and actor
118
+
119
+ A token a delegation grant issued — a client acting for a subject — carries its range as RFC 9396 `authorization_details` entries of one type, each `{ "type": <that type>, "path": "<path>" }`, and the acting client as `act.sub` (RFC 8693 §4.1). The path grammar and the containment rule are the provider–verifier [claims contract](https://github.com/o3co/auth/blob/develop/docs/claims-contract.md)'s:
120
+
121
+ - a path is `(type(:id)?.)*action`: `type` and `action` are `[a-z][a-z0-9_]*`, and `id` is `[A-Za-z0-9_~-]`, anything else percent-encoded in upper-case hex. An id is compared as written, so `%41` and `A` are two ids: the issuer and the resource must encode alike;
122
+ - an entry contains a path when it is a segment-wise prefix of it, and an entry segment that names no id contains the same type with any id: `project:p1.report` contains `project:p1.report:r7.run`. The entry's last element is compared as a type even when it names an action, so `project:p1.run` contains `project:p1.run.more`: keep action names apart from type names.
123
+
124
+ To decide the range together with the policies, configure the pair with the same `type`, read from one place:
125
+
126
+ ```hocon
127
+ attribute { collectors = [
128
+ { collector = "PayloadScopeCollector" }
129
+ # the issuer's delegation type; unset, both entries lack `type` and boot is refused
130
+ { collector = "DelegationRangeCollector", type = ${?DELEGATION_TYPE} }
131
+ # the acting client, for policies that name it
132
+ { collector = "PayloadClaimAttributeCollector", attributes = [ { from = "act.sub", to = "actorId" } ] }
133
+ ] }
134
+ rule { collectors = [
135
+ { collector = "ResourceActionScopeRuleCollector" }
136
+ { collector = "DelegationRangeRuleCollector", type = ${?DELEGATION_TYPE} }
137
+ ] }
138
+ ```
139
+
140
+ - `DelegationRangeCollector` writes the entries' paths to `ATTR_DELEGATION_RANGE` (`"delegationRange"`, reserved by this package). A path outside the grammar is left out, which narrows the range; a token with entries of the type but no readable path gets an empty range, which contains nothing, and so does a token whose claim is there in another shape — not a list, or a list holding something other than entry objects — whatever types its entries name: a token that shares `authorization_details` with other RFC 9396 types must carry it as a list of entry objects, or it is denied everything.
141
+ - `DelegationRangeRuleCollector` emits one [`WithinDelegationRange`](#withindelegationrange) rule for the requested path — `<resource.raw>.<action>`, so `resource: "project:p1.report:r7"` with `action: "run"` is `project:p1.report:r7.run` — when the token carries a range (an entry of the type, or the claim in another shape), and no rule otherwise. The action must be one action of the grammar: one of several elements (`report.delete`) would re-split the joined path, so a request on the parent would read as one on a child the range contains, and the rule fails instead. The rule is a group of its own and restricts (`restricts: true`), so a delegated token is allowed only what the range and the policies both allow: one no policy rule applies to is decided by `rule.onEmptyRuleSet` (`no_applicable_rule` by default), never by its range alone. A token without a range is decided as it is without the pair.
142
+ - Give both the same `type` and `claim` (default `authorization_details`); nothing checks that they agree. Configured apart, the pair fails one of two ways. Where the rule collector finds the token's entries and the attribute collector does not, the rule finds no range and the request is denied. The other way round, no rule is emitted and **the range is not enforced**.
143
+ - The requested path is built from the resource string the caller sent, whatever resource parser is configured. Resources written in another grammar (`projects/p1/reports/r7`) parse as no path, and every delegated request is denied.
144
+ - The resource a request names is the caller's claim, its parent chain included. A policy that relies on the chain — containment, `in` — needs it confirmed first, by a collector of your own that reads the store it lives in ([docs/extending.md](../../docs/extending.md#writing-a-custom-attributecollector)).
145
+
115
146
  ## Rules
116
147
 
117
148
  ### HasPermission
@@ -156,6 +187,10 @@ new AttrMatchRule({ a: string, b: string, group?: string })
156
187
  - 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`.
157
188
  - `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`.
158
189
 
190
+ ### WithinDelegationRange
191
+
192
+ Passes when an entry of `ATTR_DELEGATION_RANGE` contains the requested path, by the claims contract's containment rule ([Delegated tokens](#delegated-tokens-range-and-actor)). A range that is absent, or holds no string that contains the path, fails, and so does a requested path outside the grammar. `ruleType` is `delegation_range` and `code` is `outside_delegation_range`. It restricts: it narrows what the other rules allow and is no reason to allow on its own. `DelegationRangeRuleCollector` builds it. A deployment that needs another containment rule writes its own rule over `ATTR_DELEGATION_RANGE`, and a rule collector that emits it, in place of `DelegationRangeRuleCollector`.
193
+
159
194
  ## Attribute Comparison Rules
160
195
 
161
196
  The attribute comparison rules form a 2 × 5 matrix over two axes: **family** (Literal vs. Pair) and **operator** (Equal, NotEqual, In, NotIn, Compare).
@@ -263,13 +298,15 @@ Every attribute comparison rule reads each field of its config once, validates i
263
298
  | --- | --- | --- |
264
299
  | `ResourceActionPermissionRuleCollector` | `"<resource.raw>.perm:<action>"` | `[HasPermission(...)]` |
265
300
  | `ResourceActionScopeRuleCollector` | `"<action>:<resource.resourceType>"` | `[HasScope(...)]` |
301
+ | `DelegationRangeRuleCollector` | `"<resource.raw>.<action>"`, for a token with a range | `[WithinDelegationRange(...)]`, or `[]` |
266
302
 
267
- `ResourceActionPermissionRuleCollector` takes no constructor arguments.
303
+ `ResourceActionPermissionRuleCollector` takes no constructor arguments. `DelegationRangeRuleCollector` takes `{ type: string, claim?: string }` — [Delegated tokens](#delegated-tokens-range-and-actor).
268
304
  `ResourceActionScopeRuleCollector` accepts `{ scopeless?: "deny" | "skip", allowBareScopeRewrite?: boolean, claim?: string }` — `claim` names the claim whose presence says the token asserted scopes (default `scope`; set it to what `PayloadScopeCollector` reads, e.g. `scp`).
269
305
 
270
306
  - `scopeless` (default `"deny"`): it emits the `HasScope` rule for every request, so a token carrying no scope
271
- claim (`scope`, or the claim `claim` names) fails it. `"skip"` emits no rule for a scopeless token — only use it in a pipeline where another rule
272
- group authorizes the request, since a request that collects no rule at all is denied.
307
+ claim (`scope`, or the claim `claim` names) fails it. `"skip"` emits no rule for a scopeless token — only use it in a pipeline where another granting
308
+ rule group authorizes the request, since a request that collects no granting rule is denied: a restricting rule such as
309
+ [`WithinDelegationRange`](#withindelegationrange) does not authorize it.
273
310
  - `allowBareScopeRewrite` (default `false`): forwarded to [`HasScope`](#hasscope). Set it to `true` only if your
274
311
  issuer emits bare resource names (`project`) rather than `{action}:{resourceType}` scopes (`read:project`).
275
312
 
@@ -344,8 +381,10 @@ Registrations as of this writing; the source of truth is [`src/module.mts`](src/
344
381
  | `attributeCollector` | `"StaticRoleCollector"` | `(config) => new StaticRoleCollector(config)` |
345
382
  | `attributeCollector` | `"RequestContextAttributeCollector"` | `(config) => new RequestContextAttributeCollector(config)` |
346
383
  | `attributeCollector` | `"PayloadClaimAttributeCollector"` | `(config) => new PayloadClaimAttributeCollector(config)` |
384
+ | `attributeCollector` | `"DelegationRangeCollector"` | `(config) => new DelegationRangeCollector(config)` |
347
385
  | `ruleCollector` | `"ResourceActionScopeRuleCollector"` | `(config) => new ResourceActionScopeRuleCollector(config)` |
348
386
  | `ruleCollector` | `"ResourceActionPermissionRuleCollector"` | `() => new ResourceActionPermissionRuleCollector()` |
387
+ | `ruleCollector` | `"DelegationRangeRuleCollector"` | `(config) => new DelegationRangeRuleCollector(config)` |
349
388
  | `resourceParser` | `"DotNotationResourceParser"` | `() => new DotNotationResourceParser()` |
350
389
 
351
390
  ## See Also
@@ -0,0 +1,29 @@
1
+ import type { AttributeCollector, Attributes, CollectorContext } from "@o3co/auth.policy-verifier.core";
2
+ /** Config entry accepted by `DelegationRangeCollector`. */
3
+ export interface DelegationRangeCollectorConfig {
4
+ /**
5
+ * The `authorization_details` type the delegation range is carried under:
6
+ * the type the issuer's delegation grants write. Required — the same value
7
+ * `DelegationRangeRuleCollector` is given.
8
+ */
9
+ type: string;
10
+ /** The claim the entries are read from. Defaults to `authorization_details`. */
11
+ claim?: string;
12
+ }
13
+ /**
14
+ * Attribute collector that reads the paths of the token's
15
+ * `authorization_details` entries of the configured type into a `string[]`
16
+ * under `ATTR_DELEGATION_RANGE`. A path outside the grammar is left out,
17
+ * which narrows the range; a token with entries of the type but no readable
18
+ * path gets an empty range, which contains nothing, and so does a token whose
19
+ * claim is there in another shape (`entriesOfType`). A token with no entry of
20
+ * the type gets no range at all, and `DelegationRangeRuleCollector` then
21
+ * emits no rule for it.
22
+ */
23
+ export declare class DelegationRangeCollector implements AttributeCollector {
24
+ private readonly type;
25
+ private readonly claim;
26
+ constructor(config: DelegationRangeCollectorConfig);
27
+ collect(context: CollectorContext): Promise<Attributes>;
28
+ }
29
+ //# sourceMappingURL=DelegationRangeCollector.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DelegationRangeCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/DelegationRangeCollector.mts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACX,kBAAkB,EAClB,UAAU,EACV,gBAAgB,EAChB,MAAM,iCAAiC,CAAC;AAUzC,2DAA2D;AAC3D,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,qBAAa,wBAAyB,YAAW,kBAAkB;IAClE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAEnB,MAAM,EAAE,8BAA8B;IAS5C,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;CAK7D"}
@@ -0,0 +1,29 @@
1
+ // SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { DEFAULT_AUTHORIZATION_DETAILS_CLAIM, entriesOfType, rangePaths, resolveRangeType, } from "../delegation/range.mjs";
4
+ import { ATTR_DELEGATION_RANGE } from "../keys.mjs";
5
+ import { resolveClaimName } from "./_claims.mjs";
6
+ /**
7
+ * Attribute collector that reads the paths of the token's
8
+ * `authorization_details` entries of the configured type into a `string[]`
9
+ * under `ATTR_DELEGATION_RANGE`. A path outside the grammar is left out,
10
+ * which narrows the range; a token with entries of the type but no readable
11
+ * path gets an empty range, which contains nothing, and so does a token whose
12
+ * claim is there in another shape (`entriesOfType`). A token with no entry of
13
+ * the type gets no range at all, and `DelegationRangeRuleCollector` then
14
+ * emits no rule for it.
15
+ */
16
+ export class DelegationRangeCollector {
17
+ type;
18
+ claim;
19
+ constructor(config) {
20
+ this.type = resolveRangeType("DelegationRangeCollector", config?.type);
21
+ this.claim = resolveClaimName("DelegationRangeCollector", config?.claim, DEFAULT_AUTHORIZATION_DETAILS_CLAIM);
22
+ }
23
+ async collect(context) {
24
+ const entries = entriesOfType(context.subject[this.claim], this.type);
25
+ if (entries === null)
26
+ return new Map();
27
+ return new Map([[ATTR_DELEGATION_RANGE, rangePaths(entries)]]);
28
+ }
29
+ }
@@ -7,48 +7,33 @@ export type PayloadClaimAttributeType = AttributeMappingType;
7
7
  /** Config entry accepted by `PayloadClaimAttributeCollector`. */
8
8
  export type PayloadClaimAttributeCollectorConfig = AttributeMappingCollectorConfig;
9
9
  /**
10
- * Promotes declared claims of the verified subject into attributes (#219).
10
+ * Promotes declared claims of the verified subject into attributes. The
11
+ * builtins read `scope`, `sub` and `azp`; an external IdP puts what a rule
12
+ * needs elsewhere — Clerk's org role under `o.rol`, Auth0's roles under a
13
+ * namespaced `https://example.com/roles`, Okta's groups under `groups`. This
14
+ * is the declaration `RequestContextAttributeCollector` takes, pointed at
15
+ * `CollectorContext.subject` instead of the caller's body.
11
16
  *
12
- * The builtins read three claims — `scope`, `sub`, `azp` — and an external
13
- * IdP puts what a rule needs elsewhere: Clerk's org role under `o.rol`,
14
- * Auth0's roles under a namespaced `https://example.com/roles`, Okta's groups
15
- * under `groups`. Until now every one of those meant a bespoke collector,
16
- * even for the common case of "read this claim, check its type, write it
17
- * under this key". This is the same declaration `RequestContextAttributeCollector`
18
- * takes, pointed at `CollectorContext.subject` instead of the caller's body:
17
+ * The source is the subject the configured authenticator accepted — under the
18
+ * built-in one, a signature-verified token unless `oauth.jwt.mode` is
19
+ * `"insecure-decode"` — not the caller's `requestContext`, so a mapping may land
20
+ * on core's keys (`scopes`, `permissions`, `roles`, `userId`, `clientId`).
21
+ * Keys another package reserved stay refused: that package writes them with a
22
+ * collector of its own — cedar's `request*` from the parsed request, this
23
+ * package's `delegationRange` from the claim `DelegationRangeCollector`
24
+ * narrows — and a claim landing on one would be a second writer with a
25
+ * different meaning.
19
26
  *
20
- * ```hocon
21
- * { collector = "PayloadClaimAttributeCollector"
22
- * attributes = [
23
- * { from = "o.rol", to = "roles", type = "string[]" }
24
- * { from = "https://example.com/roles", to = "roles", type = "string[]" }
25
- * { from = "tid", to = "tenantId" }
26
- * ] }
27
- * ```
27
+ * Two cautions the trust argument depends on. Map only claims the IdP
28
+ * populates from its own registration or admin data, never from user-editable
29
+ * profile metadata (Clerk's `unsafe_metadata`, Auth0's `user_metadata`) — a
30
+ * mapping from those hands the end user their own roles. And two collectors
31
+ * writing one list key union it, but a scalar key written by two collectors
32
+ * with different values throws `AttributeConflictError` and denies every
33
+ * request: do not map onto `userId` / `clientId` while
34
+ * `PayloadSubjectIdCollector` writes them.
28
35
  *
29
- * ## Core's vocabulary is a valid destination here
30
- *
31
- * `RequestContextAttributeCollector` refuses to write `scopes`, `permissions`,
32
- * `roles`, `userId` and `clientId`, because its source is the request body
33
- * and those keys are what the engine decides from. This collector's source is
34
- * the signature-verified token — the same trust `PayloadScopeCollector` writes
35
- * `scopes` from — so a verified claim may land on them: `permissions` from
36
- * Auth0's `permissions`, `roles` from Clerk's `o.rol`, `scopes` from Okta's
37
- * `scp`. Two collectors writing one list key **union** it; that is the
38
- * deployment composing two issuer-derived sources, and it says so in config.
39
- *
40
- * Keys another package reserved — cedar's `request*` — stay refused: those
41
- * are derived from the parsed request, not from the subject, and a claim
42
- * landing on one would be a second writer with a different meaning.
43
- *
44
- * Two cautions the trust argument depends on. A signed claim is only as
45
- * trustworthy as what the IdP put in it: map claims the IdP populates from
46
- * its own registration or admin data, never from user-editable profile
47
- * metadata (Clerk's `unsafe_metadata`, Auth0's `user_metadata`) — a mapping
48
- * from those hands the end user their own roles. And the union rule is for
49
- * lists: a *scalar* key written by two collectors with different values
50
- * throws `AttributeConflictError` and denies every request, so do not map
51
- * onto `userId` / `clientId` while `PayloadSubjectIdCollector` writes them.
36
+ * Configuration, with an example: the package README, PayloadClaimAttributeCollector.
52
37
  */
53
38
  export declare class PayloadClaimAttributeCollector implements AttributeCollector {
54
39
  private readonly mappings;
@@ -1 +1 @@
1
- {"version":3,"file":"PayloadClaimAttributeCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/PayloadClaimAttributeCollector.mts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,kBAAkB,EAElB,UAAU,EACV,gBAAgB,EAChB,MAAM,iCAAiC,CAAC;AAKzC,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,+BAA+B,EACpC,KAAK,oBAAoB,EAIzB,MAAM,yBAAyB,CAAC;AAEjC,yEAAyE;AACzE,MAAM,MAAM,4BAA4B,GAAG,gBAAgB,CAAC;AAC5D,wCAAwC;AACxC,MAAM,MAAM,yBAAyB,GAAG,oBAAoB,CAAC;AAC7D,iEAAiE;AACjE,MAAM,MAAM,oCAAoC,GAAG,+BAA+B,CAAC;AAEnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,8BAA+B,YAAW,kBAAkB;IACxE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;gBAE1C,MAAM,EAAE,oCAAoC;IASlD,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;CAK7D"}
1
+ {"version":3,"file":"PayloadClaimAttributeCollector.d.mts","sourceRoot":"","sources":["../../src/collectors/PayloadClaimAttributeCollector.mts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACX,kBAAkB,EAElB,UAAU,EACV,gBAAgB,EAChB,MAAM,iCAAiC,CAAC;AAKzC,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,+BAA+B,EACpC,KAAK,oBAAoB,EAIzB,MAAM,yBAAyB,CAAC;AAEjC,yEAAyE;AACzE,MAAM,MAAM,4BAA4B,GAAG,gBAAgB,CAAC;AAC5D,wCAAwC;AACxC,MAAM,MAAM,yBAAyB,GAAG,oBAAoB,CAAC;AAC7D,iEAAiE;AACjE,MAAM,MAAM,oCAAoC,GAAG,+BAA+B,CAAC;AAEnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,8BAA+B,YAAW,kBAAkB;IACxE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;gBAE1C,MAAM,EAAE,oCAAoC;IASlD,OAAO,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC;CAK7D"}
@@ -3,48 +3,33 @@
3
3
  import { CORE_ATTRIBUTE_KEY_OWNER, suggestUnreservedAttributeKey, } from "@o3co/auth.policy-verifier.core";
4
4
  import { parseAttributeMappings, promoteMappings, } from "./_attributeMapping.mjs";
5
5
  /**
6
- * Promotes declared claims of the verified subject into attributes (#219).
6
+ * Promotes declared claims of the verified subject into attributes. The
7
+ * builtins read `scope`, `sub` and `azp`; an external IdP puts what a rule
8
+ * needs elsewhere — Clerk's org role under `o.rol`, Auth0's roles under a
9
+ * namespaced `https://example.com/roles`, Okta's groups under `groups`. This
10
+ * is the declaration `RequestContextAttributeCollector` takes, pointed at
11
+ * `CollectorContext.subject` instead of the caller's body.
7
12
  *
8
- * The builtins read three claims — `scope`, `sub`, `azp` — and an external
9
- * IdP puts what a rule needs elsewhere: Clerk's org role under `o.rol`,
10
- * Auth0's roles under a namespaced `https://example.com/roles`, Okta's groups
11
- * under `groups`. Until now every one of those meant a bespoke collector,
12
- * even for the common case of "read this claim, check its type, write it
13
- * under this key". This is the same declaration `RequestContextAttributeCollector`
14
- * takes, pointed at `CollectorContext.subject` instead of the caller's body:
13
+ * The source is the subject the configured authenticator accepted — under the
14
+ * built-in one, a signature-verified token unless `oauth.jwt.mode` is
15
+ * `"insecure-decode"` — not the caller's `requestContext`, so a mapping may land
16
+ * on core's keys (`scopes`, `permissions`, `roles`, `userId`, `clientId`).
17
+ * Keys another package reserved stay refused: that package writes them with a
18
+ * collector of its own — cedar's `request*` from the parsed request, this
19
+ * package's `delegationRange` from the claim `DelegationRangeCollector`
20
+ * narrows — and a claim landing on one would be a second writer with a
21
+ * different meaning.
15
22
  *
16
- * ```hocon
17
- * { collector = "PayloadClaimAttributeCollector"
18
- * attributes = [
19
- * { from = "o.rol", to = "roles", type = "string[]" }
20
- * { from = "https://example.com/roles", to = "roles", type = "string[]" }
21
- * { from = "tid", to = "tenantId" }
22
- * ] }
23
- * ```
23
+ * Two cautions the trust argument depends on. Map only claims the IdP
24
+ * populates from its own registration or admin data, never from user-editable
25
+ * profile metadata (Clerk's `unsafe_metadata`, Auth0's `user_metadata`) — a
26
+ * mapping from those hands the end user their own roles. And two collectors
27
+ * writing one list key union it, but a scalar key written by two collectors
28
+ * with different values throws `AttributeConflictError` and denies every
29
+ * request: do not map onto `userId` / `clientId` while
30
+ * `PayloadSubjectIdCollector` writes them.
24
31
  *
25
- * ## Core's vocabulary is a valid destination here
26
- *
27
- * `RequestContextAttributeCollector` refuses to write `scopes`, `permissions`,
28
- * `roles`, `userId` and `clientId`, because its source is the request body
29
- * and those keys are what the engine decides from. This collector's source is
30
- * the signature-verified token — the same trust `PayloadScopeCollector` writes
31
- * `scopes` from — so a verified claim may land on them: `permissions` from
32
- * Auth0's `permissions`, `roles` from Clerk's `o.rol`, `scopes` from Okta's
33
- * `scp`. Two collectors writing one list key **union** it; that is the
34
- * deployment composing two issuer-derived sources, and it says so in config.
35
- *
36
- * Keys another package reserved — cedar's `request*` — stay refused: those
37
- * are derived from the parsed request, not from the subject, and a claim
38
- * landing on one would be a second writer with a different meaning.
39
- *
40
- * Two cautions the trust argument depends on. A signed claim is only as
41
- * trustworthy as what the IdP put in it: map claims the IdP populates from
42
- * its own registration or admin data, never from user-editable profile
43
- * metadata (Clerk's `unsafe_metadata`, Auth0's `user_metadata`) — a mapping
44
- * from those hands the end user their own roles. And the union rule is for
45
- * lists: a *scalar* key written by two collectors with different values
46
- * throws `AttributeConflictError` and denies every request, so do not map
47
- * onto `userId` / `clientId` while `PayloadSubjectIdCollector` writes them.
32
+ * Configuration, with an example: the package README, PayloadClaimAttributeCollector.
48
33
  */
49
34
  export class PayloadClaimAttributeCollector {
50
35
  mappings;
@@ -62,7 +47,7 @@ function refusal(index, reservation) {
62
47
  const { key, owner, reason } = reservation;
63
48
  return (`PayloadClaimAttributeCollector: attributes[${index}] maps onto the reserved attribute "${key}", ` +
64
49
  `which belongs to ${owner}${reason === undefined ? "" : ` — ${reason}`}. ` +
65
- "That package derives the key from the request, not from the subject, so a verified claim " +
66
- "landing on it would be a second writer with a different meaning. " +
50
+ "That package writes the key with a collector of its own, so a verified claim landing on it " +
51
+ "would be a second writer with a different meaning. " +
67
52
  `Promote the claim under a key of your own (for example "${suggestUnreservedAttributeKey(key)}").`);
68
53
  }
@@ -2,7 +2,7 @@ import type { AttributeCollector, Attributes, CollectorContext } from "@o3co/aut
2
2
  /** Config entry accepted by `PayloadScopeCollector`. */
3
3
  export interface PayloadScopeCollectorConfig {
4
4
  /**
5
- * The claim the scopes are read from (#219). Defaults to `scope`, the
5
+ * The claim the scopes are read from. Defaults to `scope`, the
6
6
  * space-delimited OAuth claim; `scp` for Okta, `permissions` for Auth0,
7
7
  * both of which are arrays of strings. Either shape is read.
8
8
  */
@@ -4,7 +4,7 @@ import type { AttributeCollector, Attributes, CollectorContext } from "@o3co/aut
4
4
  * the subject bag into `ATTR_USER_ID` and `ATTR_CLIENT_ID`. Either claim may
5
5
  * be absent.
6
6
  *
7
- * The claim vocabulary lives here, not in core (#170): `SubjectAttributes` is
7
+ * The claim vocabulary lives here, not in core: `SubjectAttributes` is
8
8
  * a bag of unknowns, so this collector narrows what it promotes — a claim that
9
9
  * is not a non-empty string is not an identity and is left out.
10
10
  */
@@ -6,7 +6,7 @@ import { ATTR_CLIENT_ID, ATTR_USER_ID } from "@o3co/auth.policy-verifier.core";
6
6
  * the subject bag into `ATTR_USER_ID` and `ATTR_CLIENT_ID`. Either claim may
7
7
  * be absent.
8
8
  *
9
- * The claim vocabulary lives here, not in core (#170): `SubjectAttributes` is
9
+ * The claim vocabulary lives here, not in core: `SubjectAttributes` is
10
10
  * a bag of unknowns, so this collector narrows what it promotes — a claim that
11
11
  * is not a non-empty string is not an identity and is left out.
12
12
  */