@twin.org/rights-management-plugins 0.0.3-next.4 → 0.0.3-next.41

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 (131) hide show
  1. package/README.md +11 -3
  2. package/dist/es/index.js +18 -10
  3. package/dist/es/index.js.map +1 -1
  4. package/dist/es/models/IAutomationPolicyExecutionActionConfig.js +4 -0
  5. package/dist/es/models/IAutomationPolicyExecutionActionConfig.js.map +1 -0
  6. package/dist/es/models/IAutomationPolicyExecutionActionConstructorOptions.js +2 -0
  7. package/dist/es/models/IAutomationPolicyExecutionActionConstructorOptions.js.map +1 -0
  8. package/dist/es/models/{IExamplePolicyArbiterConstructorOptions.js → IDefaultPolicyArbiterConfig.js} +1 -1
  9. package/dist/es/models/IDefaultPolicyArbiterConfig.js.map +1 -0
  10. package/dist/es/models/{IExamplePolicyRequesterConstructorOptions.js → IDefaultPolicyArbiterConstructorOptions.js} +1 -1
  11. package/dist/es/models/IDefaultPolicyArbiterConstructorOptions.js.map +1 -0
  12. package/dist/es/models/{IExamplePolicyEnforcementProcessorConstructorOptions.js → IDefaultPolicyEnforcementProcessorConstructorOptions.js} +1 -1
  13. package/dist/es/models/IDefaultPolicyEnforcementProcessorConstructorOptions.js.map +1 -0
  14. package/dist/es/models/ILoggingPolicyExecutionActionConfig.js.map +1 -1
  15. package/dist/es/models/{IExampleDataAccessHandlerConstructorOptions.js → IPassThroughPolicyArbiterConstructorOptions.js} +1 -1
  16. package/dist/es/models/IPassThroughPolicyArbiterConstructorOptions.js.map +1 -0
  17. package/dist/es/models/IPassThroughPolicyEnforcementProcessorConstructorOptions.js +4 -0
  18. package/dist/es/models/IPassThroughPolicyEnforcementProcessorConstructorOptions.js.map +1 -0
  19. package/dist/es/models/IPassThroughPolicyNegotiatorConstructorOptions.js +4 -0
  20. package/dist/es/models/IPassThroughPolicyNegotiatorConstructorOptions.js.map +1 -0
  21. package/dist/es/models/IPassThroughPolicyObligationEnforcerConstructorOptions.js +4 -0
  22. package/dist/es/models/IPassThroughPolicyObligationEnforcerConstructorOptions.js.map +1 -0
  23. package/dist/es/models/{IExamplePolicyNegotiatorConstructorOptions.js → IPassThroughPolicyRequesterConstructorOptions.js} +1 -1
  24. package/dist/es/models/IPassThroughPolicyRequesterConstructorOptions.js.map +1 -0
  25. package/dist/es/models/IStaticPolicyInformationSource.js.map +1 -1
  26. package/dist/es/policyArbiters/defaultPolicyArbiter.js +1657 -0
  27. package/dist/es/policyArbiters/defaultPolicyArbiter.js.map +1 -0
  28. package/dist/es/policyArbiters/passThroughPolicyArbiter.js +59 -0
  29. package/dist/es/policyArbiters/passThroughPolicyArbiter.js.map +1 -0
  30. package/dist/es/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.js +146 -0
  31. package/dist/es/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.js.map +1 -0
  32. package/dist/es/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.js +54 -0
  33. package/dist/es/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.js.map +1 -0
  34. package/dist/es/policyExecutionActions/automationPolicyExecutionAction.js +76 -0
  35. package/dist/es/policyExecutionActions/automationPolicyExecutionAction.js.map +1 -0
  36. package/dist/es/policyExecutionActions/loggingPolicyExecutionAction.js +16 -15
  37. package/dist/es/policyExecutionActions/loggingPolicyExecutionAction.js.map +1 -1
  38. package/dist/es/policyInformationSources/identityPolicyInformationSource.js +43 -31
  39. package/dist/es/policyInformationSources/identityPolicyInformationSource.js.map +1 -1
  40. package/dist/es/policyInformationSources/staticPolicyInformationSource.js +17 -15
  41. package/dist/es/policyInformationSources/staticPolicyInformationSource.js.map +1 -1
  42. package/dist/es/policyNegotiators/passThroughPolicyNegotiator.js +88 -0
  43. package/dist/es/policyNegotiators/passThroughPolicyNegotiator.js.map +1 -0
  44. package/dist/es/policyObligationEnforcers/passThroughPolicyObligationEnforcer.js +56 -0
  45. package/dist/es/policyObligationEnforcers/passThroughPolicyObligationEnforcer.js.map +1 -0
  46. package/dist/es/policyRequesters/passThroughPolicyRequester.js +103 -0
  47. package/dist/es/policyRequesters/passThroughPolicyRequester.js.map +1 -0
  48. package/dist/types/index.d.ts +18 -10
  49. package/dist/types/models/IAutomationPolicyExecutionActionConfig.d.ts +9 -0
  50. package/dist/types/models/IAutomationPolicyExecutionActionConstructorOptions.d.ts +15 -0
  51. package/dist/types/models/IDefaultPolicyArbiterConfig.d.ts +10 -0
  52. package/dist/types/models/IDefaultPolicyArbiterConstructorOptions.d.ts +20 -0
  53. package/dist/types/models/{IExamplePolicyEnforcementProcessorConstructorOptions.d.ts → IDefaultPolicyEnforcementProcessorConstructorOptions.d.ts} +2 -2
  54. package/dist/types/models/ILoggingPolicyExecutionActionConfig.d.ts +2 -2
  55. package/dist/types/models/{IExamplePolicyArbiterConstructorOptions.d.ts → IPassThroughPolicyArbiterConstructorOptions.d.ts} +2 -2
  56. package/dist/types/models/IPassThroughPolicyEnforcementProcessorConstructorOptions.d.ts +10 -0
  57. package/dist/types/models/{IExamplePolicyNegotiatorConstructorOptions.d.ts → IPassThroughPolicyNegotiatorConstructorOptions.d.ts} +2 -2
  58. package/dist/types/models/IPassThroughPolicyObligationEnforcerConstructorOptions.d.ts +10 -0
  59. package/dist/types/models/{IExamplePolicyRequesterConstructorOptions.d.ts → IPassThroughPolicyRequesterConstructorOptions.d.ts} +2 -2
  60. package/dist/types/models/IStaticPolicyInformationSource.d.ts +10 -4
  61. package/dist/types/policyArbiters/defaultPolicyArbiter.d.ts +41 -0
  62. package/dist/types/policyArbiters/passThroughPolicyArbiter.d.ts +35 -0
  63. package/dist/types/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.d.ts +40 -0
  64. package/dist/types/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.d.ts +32 -0
  65. package/dist/types/policyExecutionActions/automationPolicyExecutionAction.d.ts +49 -0
  66. package/dist/types/policyExecutionActions/loggingPolicyExecutionAction.d.ts +8 -8
  67. package/dist/types/policyInformationSources/identityPolicyInformationSource.d.ts +8 -6
  68. package/dist/types/policyInformationSources/staticPolicyInformationSource.d.ts +7 -5
  69. package/dist/types/policyNegotiators/passThroughPolicyNegotiator.d.ts +52 -0
  70. package/dist/types/policyObligationEnforcers/passThroughPolicyObligationEnforcer.d.ts +35 -0
  71. package/dist/types/policyRequesters/{examplePolicyRequester.d.ts → passThroughPolicyRequester.d.ts} +15 -20
  72. package/docs/changelog.md +835 -3
  73. package/docs/examples.md +295 -1
  74. package/docs/reference/classes/AutomationPolicyExecutionAction.md +143 -0
  75. package/docs/reference/classes/DefaultPolicyArbiter.md +111 -0
  76. package/docs/reference/classes/DefaultPolicyEnforcementProcessor.md +107 -0
  77. package/docs/reference/classes/IdentityPolicyInformationSource.md +14 -14
  78. package/docs/reference/classes/LoggingPolicyExecutionAction.md +20 -20
  79. package/docs/reference/classes/PassThroughPolicyArbiter.md +101 -0
  80. package/docs/reference/classes/PassThroughPolicyEnforcementProcessor.md +107 -0
  81. package/docs/reference/classes/{ExamplePolicyNegotiator.md → PassThroughPolicyNegotiator.md} +24 -22
  82. package/docs/reference/classes/PassThroughPolicyObligationEnforcer.md +107 -0
  83. package/docs/reference/classes/{ExamplePolicyRequester.md → PassThroughPolicyRequester.md} +20 -38
  84. package/docs/reference/classes/StaticPolicyInformationSource.md +15 -15
  85. package/docs/reference/index.md +18 -10
  86. package/docs/reference/interfaces/IAutomationPolicyExecutionActionConfig.md +11 -0
  87. package/docs/reference/interfaces/IAutomationPolicyExecutionActionConstructorOptions.md +25 -0
  88. package/docs/reference/interfaces/IDefaultPolicyArbiterConfig.md +17 -0
  89. package/docs/reference/interfaces/IDefaultPolicyArbiterConstructorOptions.md +39 -0
  90. package/docs/reference/interfaces/IDefaultPolicyEnforcementProcessorConstructorOptions.md +17 -0
  91. package/docs/reference/interfaces/IIdentityPolicyInformationSourceConstructorOptions.md +4 -4
  92. package/docs/reference/interfaces/ILoggingPolicyExecutionActionConfig.md +9 -9
  93. package/docs/reference/interfaces/ILoggingPolicyExecutionActionConstructorOptions.md +4 -4
  94. package/docs/reference/interfaces/IPassThroughPolicyArbiterConstructorOptions.md +17 -0
  95. package/docs/reference/interfaces/IPassThroughPolicyEnforcementProcessorConstructorOptions.md +17 -0
  96. package/docs/reference/interfaces/IPassThroughPolicyNegotiatorConstructorOptions.md +17 -0
  97. package/docs/reference/interfaces/IPassThroughPolicyObligationEnforcerConstructorOptions.md +17 -0
  98. package/docs/reference/interfaces/IPassThroughPolicyRequesterConstructorOptions.md +17 -0
  99. package/docs/reference/interfaces/IStaticPolicyInformationSource.md +25 -6
  100. package/docs/reference/interfaces/IStaticPolicyInformationSourceConfig.md +2 -2
  101. package/docs/reference/interfaces/IStaticPolicyInformationSourceConstructorOptions.md +4 -4
  102. package/locales/en.json +58 -6
  103. package/package.json +7 -5
  104. package/dist/es/dataAccessHandlers/examplePolicyRequester.js +0 -82
  105. package/dist/es/dataAccessHandlers/examplePolicyRequester.js.map +0 -1
  106. package/dist/es/models/IExampleDataAccessHandlerConstructorOptions.js.map +0 -1
  107. package/dist/es/models/IExamplePolicyArbiterConstructorOptions.js.map +0 -1
  108. package/dist/es/models/IExamplePolicyEnforcementProcessorConstructorOptions.js.map +0 -1
  109. package/dist/es/models/IExamplePolicyNegotiatorConstructorOptions.js.map +0 -1
  110. package/dist/es/models/IExamplePolicyRequesterConstructorOptions.js.map +0 -1
  111. package/dist/es/policyArbiters/examplePolicyArbiter.js +0 -50
  112. package/dist/es/policyArbiters/examplePolicyArbiter.js.map +0 -1
  113. package/dist/es/policyEnforcementProcessor/examplePolicyEnforcementProcessor.js +0 -42
  114. package/dist/es/policyEnforcementProcessor/examplePolicyEnforcementProcessor.js.map +0 -1
  115. package/dist/es/policyNegotiators/examplePolicyNegotiator.js +0 -61
  116. package/dist/es/policyNegotiators/examplePolicyNegotiator.js.map +0 -1
  117. package/dist/es/policyRequesters/examplePolicyRequester.js +0 -69
  118. package/dist/es/policyRequesters/examplePolicyRequester.js.map +0 -1
  119. package/dist/types/dataAccessHandlers/examplePolicyRequester.d.ts +0 -68
  120. package/dist/types/models/IExampleDataAccessHandlerConstructorOptions.d.ts +0 -10
  121. package/dist/types/policyArbiters/examplePolicyArbiter.d.ts +0 -36
  122. package/dist/types/policyEnforcementProcessor/examplePolicyEnforcementProcessor.d.ts +0 -29
  123. package/dist/types/policyNegotiators/examplePolicyNegotiator.d.ts +0 -45
  124. package/docs/reference/classes/ExampleDataAccessHandler.md +0 -243
  125. package/docs/reference/classes/ExamplePolicyArbiter.md +0 -121
  126. package/docs/reference/classes/ExamplePolicyEnforcementProcessor.md +0 -101
  127. package/docs/reference/interfaces/IExampleDataAccessHandlerConstructorOptions.md +0 -17
  128. package/docs/reference/interfaces/IExamplePolicyArbiterConstructorOptions.md +0 -17
  129. package/docs/reference/interfaces/IExamplePolicyEnforcementProcessorConstructorOptions.md +0 -17
  130. package/docs/reference/interfaces/IExamplePolicyNegotiatorConstructorOptions.md +0 -17
  131. package/docs/reference/interfaces/IExamplePolicyRequesterConstructorOptions.md +0 -17
@@ -0,0 +1,1657 @@
1
+ // Copyright 2025 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { ArrayHelper, Coerce, ComponentFactory, GeneralError, Guards, Is, ObjectHelper, StringHelper } from "@twin.org/core";
4
+ import { JsonPathHelper } from "@twin.org/data-json-path";
5
+ import { OdrlPolicyHelper, OdrlProfiles, PolicyDecision, PolicyObligationEnforcerFactory } from "@twin.org/rights-management-models";
6
+ import { OdrlConflictStrategyType, OdrlLogicalConstraintType, OdrlOperatorType, OdrlTypes } from "@twin.org/standards-w3c-odrl";
7
+ /**
8
+ * Default Policy Arbiter.
9
+ */
10
+ export class DefaultPolicyArbiter {
11
+ /**
12
+ * The class name of the Default Policy Arbiter.
13
+ */
14
+ static CLASS_NAME = "DefaultPolicyArbiter";
15
+ /**
16
+ * ODRL profiles whose custom vocabulary this arbiter understands and supports.
17
+ * Any policy declaring a profile not in this set will be rejected.
18
+ * Add a new entry here when support for an additional profile is implemented.
19
+ */
20
+ static SUPPORTED_PROFILES = new Set([OdrlProfiles.Twin]);
21
+ /**
22
+ * Default maximum inheritance depth.
23
+ * @internal
24
+ */
25
+ static _DEFAULT_MAX_INHERITANCE_DEPTH = 10;
26
+ /**
27
+ * Datasource key for the primary JSON path data source.
28
+ * @internal
29
+ */
30
+ static _DATA_SOURCE_KEY = "data";
31
+ /**
32
+ * Datasource key for the information source used in JSON path expressions.
33
+ * @internal
34
+ */
35
+ static _INFORMATION_SOURCE_KEY = "information";
36
+ /**
37
+ * TWIN prefix JSONPath canonical alias.
38
+ * @internal
39
+ */
40
+ static _TWIN_JSONPATH = "twin:jsonPath";
41
+ /**
42
+ * Canonical jsonPath expression property.
43
+ * @internal
44
+ */
45
+ static _TWIN_JSONPATH_EXPRESSION = "twin:jsonPathExpression";
46
+ /**
47
+ * Optional data source key property for canonical twin:jsonPath targets.
48
+ * When absent, defaults to the primary data source.
49
+ * @internal
50
+ */
51
+ static _TWIN_JSONPATH_DATA_SOURCE = "twin:jsonPathDataSource";
52
+ /**
53
+ * The logging component.
54
+ * @internal
55
+ */
56
+ _logging;
57
+ /**
58
+ * The policy administration point component.
59
+ * @internal
60
+ */
61
+ _policyAdministrationPoint;
62
+ /**
63
+ * The maximum depth to traverse when resolving inherited policies.
64
+ * @internal
65
+ */
66
+ _maxInheritanceDepth;
67
+ /**
68
+ * Create a new instance of DefaultPolicyArbiter.
69
+ * @param options The options for the default policy arbiter.
70
+ */
71
+ constructor(options) {
72
+ this._logging = ComponentFactory.get(options?.loggingComponentType ?? "logging");
73
+ this._policyAdministrationPoint = ComponentFactory.get(options?.policyAdministrationPointComponentType ?? "policy-administration-point");
74
+ this._maxInheritanceDepth =
75
+ Coerce.integer(options?.config?.maxInheritanceDepth) ??
76
+ DefaultPolicyArbiter._DEFAULT_MAX_INHERITANCE_DEPTH;
77
+ }
78
+ /**
79
+ * Normalises a profile IRI for comparison against the supported-profiles allowlist.
80
+ * Per RFC 3986: scheme and host are case-insensitive (the URL constructor folds them to
81
+ * lowercase automatically); a trailing slash on the terminal path segment is treated as
82
+ * equivalent to its absence; repeated slashes in the path are collapsed to a single slash.
83
+ * Non-URL strings are returned unchanged.
84
+ * @param iri The IRI to normalise.
85
+ * @returns The normalised IRI.
86
+ * @internal
87
+ */
88
+ static normalizeProfileIri(iri) {
89
+ try {
90
+ const url = new URL(iri);
91
+ const pathname = url.pathname.replace(/\/+/g, "/");
92
+ return `${url.protocol}//${url.host}${StringHelper.trimTrailingSlashes(pathname)}`;
93
+ }
94
+ catch {
95
+ return iri;
96
+ }
97
+ }
98
+ /**
99
+ * Returns the class name of the component.
100
+ * @returns The class name of the component.
101
+ */
102
+ className() {
103
+ return DefaultPolicyArbiter.CLASS_NAME;
104
+ }
105
+ /**
106
+ * Makes decisions regarding policy access to data.
107
+ * @param agreement The agreement to evaluate.
108
+ * @param information Information provided by the requester to determine if a policy can be created.
109
+ * @param data The data to make a decision on.
110
+ * @param action Optional action to make a decision on, if not provided, the arbiter will evaluate all actions in the agreement.
111
+ * @returns The decisions about access to the data.
112
+ */
113
+ async decide(agreement, information, data, action) {
114
+ Guards.object(DefaultPolicyArbiter.CLASS_NAME, "agreement", agreement);
115
+ // ODRL policy profiles extend the vocabulary with additional semantics (e.g. custom
116
+ // operators, left operands). Without profile-aware evaluation logic the arbiter
117
+ // cannot guarantee correctness, so any policy that declares an unknown profile is
118
+ // rejected. The TWIN platform profile is explicitly supported.
119
+ //
120
+ // Empty-string profile values (e.g. from over-eager schema defaults or serialization
121
+ // round-trips) are treated as "no profile declared" and filtered out before comparison.
122
+ //
123
+ // Profile IRIs are normalized before lookup: scheme and host are case-folded to
124
+ // lowercase and a trailing slash on the last path segment is stripped. This accepts
125
+ // common IRI variants (HTTPS://, uppercase host, trailing slash) instead of silently
126
+ // rejecting valid policies authored by IRI-aware tooling.
127
+ //
128
+ // `every` (conjunction) is intentional: a policy declaring ["TWIN", "unknown"] is
129
+ // rejected — the arbiter refuses to evaluate rules from a profile whose semantics it
130
+ // does not understand, even if other declared profiles are known.
131
+ const declaredProfiles = ArrayHelper.fromObjectOrArray(agreement.profile ?? [])
132
+ .filter(p => Is.stringValue(p))
133
+ .map(p => DefaultPolicyArbiter.normalizeProfileIri(p));
134
+ if (Is.arrayValue(declaredProfiles) &&
135
+ !declaredProfiles.every(p => DefaultPolicyArbiter.SUPPORTED_PROFILES.has(p))) {
136
+ const unsupportedProfile = declaredProfiles.find(p => !DefaultPolicyArbiter.SUPPORTED_PROFILES.has(p));
137
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "policyProfileNotSupported", {
138
+ policyId: OdrlPolicyHelper.getUid(agreement) ?? "",
139
+ unsupportedProfile
140
+ });
141
+ }
142
+ await this._logging.log({
143
+ level: "info",
144
+ source: DefaultPolicyArbiter.CLASS_NAME,
145
+ ts: Date.now(),
146
+ message: "decidingPolicy",
147
+ data: {
148
+ policyId: OdrlPolicyHelper.getUid(agreement) ?? ""
149
+ }
150
+ });
151
+ // Resolve and merge inherited policies.
152
+ const mergedPolicy = await this.mergeInheritedPolicies(agreement);
153
+ const expandedPolicy = this.expandCompactPolicyRules(mergedPolicy);
154
+ const dataSources = {
155
+ [DefaultPolicyArbiter._DATA_SOURCE_KEY]: data,
156
+ [DefaultPolicyArbiter._INFORMATION_SOURCE_KEY]: information
157
+ };
158
+ // Extract agreement parties once for use in rule evaluation
159
+ const agreementAssigner = OdrlPolicyHelper.getPartyIds(agreement.assigner);
160
+ const agreementAssignee = OdrlPolicyHelper.getPartyIds(agreement.assignee);
161
+ const obligationsFulfilled = await this.evaluatePolicyObligations(agreementAssigner, agreementAssignee, expandedPolicy, dataSources);
162
+ // ODRL-style rule evaluation grouped by decision target:
163
+ // - Permission rules authorize if ANY applicable permission on the same target matches.
164
+ // - Default to denied when no permission applies on a target (closed-world for access control).
165
+ // - Conflict strategy controls how applicable permissions/prohibitions are resolved per target.
166
+ const targetStates = Object.create(null);
167
+ const permissions = ArrayHelper.fromObjectOrArray(expandedPolicy.permission ?? []);
168
+ for (const permission of permissions) {
169
+ const decisionTargets = this.resolveRuleDecisionTargets(permission, dataSources);
170
+ for (const decisionTarget of decisionTargets) {
171
+ const state = this.getOrCreateTargetState(targetStates, decisionTarget.target);
172
+ if (await this.evaluatePermission(agreementAssigner, agreementAssignee, expandedPolicy, permission, decisionTarget.refinements, dataSources, action, decisionTarget.target)) {
173
+ state.permissionApplies = true;
174
+ }
175
+ }
176
+ }
177
+ const prohibitions = ArrayHelper.fromObjectOrArray(expandedPolicy.prohibition ?? []);
178
+ for (const prohibition of prohibitions) {
179
+ const decisionTargets = this.resolveRuleDecisionTargets(prohibition, dataSources);
180
+ for (const decisionTarget of decisionTargets) {
181
+ if (await this.evaluateProhibition(expandedPolicy, agreementAssigner, agreementAssignee, prohibition, decisionTarget.refinements, dataSources, action, decisionTarget.target)) {
182
+ const state = this.getOrCreateTargetState(targetStates, decisionTarget.target);
183
+ state.prohibitionApplies = true;
184
+ }
185
+ }
186
+ }
187
+ const conflictStrategy = expandedPolicy.conflict ?? OdrlConflictStrategyType.Invalid;
188
+ if (Object.keys(targetStates).length === 0) {
189
+ // Closed-world fallback when the policy has no rules at all.
190
+ return [{ decision: PolicyDecision.Denied, target: "$" }];
191
+ }
192
+ const decisions = [];
193
+ for (const [target, state] of Object.entries(targetStates)) {
194
+ let decision;
195
+ if (state.permissionApplies && state.prohibitionApplies) {
196
+ switch (conflictStrategy) {
197
+ case OdrlConflictStrategyType.Perm:
198
+ decision = PolicyDecision.Granted;
199
+ break;
200
+ case OdrlConflictStrategyType.Prohibit:
201
+ case OdrlConflictStrategyType.Invalid:
202
+ default:
203
+ decision = PolicyDecision.Denied;
204
+ break;
205
+ }
206
+ }
207
+ else {
208
+ decision = state.permissionApplies ? PolicyDecision.Granted : PolicyDecision.Denied;
209
+ }
210
+ if (!obligationsFulfilled) {
211
+ decision = PolicyDecision.Denied;
212
+ }
213
+ decisions.push({
214
+ decision,
215
+ target
216
+ });
217
+ }
218
+ return decisions;
219
+ }
220
+ /**
221
+ * Expand compact/compound policy rule forms into atomic rules.
222
+ * ODRL 2.7 allows compact forms where rule properties can be arrays.
223
+ * Evaluation in this arbiter is performed on expanded atomic rules.
224
+ * @param policy The policy to expand.
225
+ * @returns A policy with expanded rule arrays.
226
+ * @internal
227
+ */
228
+ expandCompactPolicyRules(policy) {
229
+ const expandedPermissions = this.expandRules(ArrayHelper.fromObjectOrArray(policy.permission ?? []));
230
+ const expandedProhibitions = this.expandRules(ArrayHelper.fromObjectOrArray(policy.prohibition ?? []));
231
+ const expandedObligations = this.expandRules(ArrayHelper.fromObjectOrArray(policy.obligation ?? []));
232
+ return {
233
+ ...policy,
234
+ permission: expandedPermissions.length > 0 ? expandedPermissions : undefined,
235
+ prohibition: expandedProhibitions.length > 0 ? expandedProhibitions : undefined,
236
+ obligation: expandedObligations.length > 0 ? expandedObligations : undefined
237
+ };
238
+ }
239
+ /**
240
+ * Expand a rule list into atomic rules.
241
+ * @param rules The rules to expand.
242
+ * @returns Expanded atomic rules.
243
+ * @internal
244
+ */
245
+ expandRules(rules) {
246
+ const expanded = [];
247
+ for (const rule of rules) {
248
+ expanded.push(...this.expandRule(rule));
249
+ }
250
+ return expanded;
251
+ }
252
+ /**
253
+ * Expand a single compact/compound rule into atomic rules.
254
+ * @param rule The rule to expand.
255
+ * @returns Expanded atomic rules.
256
+ * @internal
257
+ */
258
+ expandRule(rule) {
259
+ const targets = this.normalizeRuleField(rule.target);
260
+ const actions = this.normalizeRuleField(rule.action);
261
+ const assigners = this.normalizeRuleField(rule.assigner);
262
+ const assignees = this.normalizeRuleField(rule.assignee);
263
+ const expanded = [];
264
+ for (const target of targets) {
265
+ for (const action of actions) {
266
+ for (const assigner of assigners) {
267
+ for (const assignee of assignees) {
268
+ const atomicRule = { ...rule };
269
+ atomicRule.target = target;
270
+ atomicRule.action = action;
271
+ atomicRule.assigner = assigner;
272
+ atomicRule.assignee = assignee;
273
+ expanded.push(atomicRule);
274
+ }
275
+ }
276
+ }
277
+ }
278
+ return expanded;
279
+ }
280
+ /**
281
+ * Normalize a potentially compact rule field into an array for expansion.
282
+ * @param value The field value.
283
+ * @returns Normalized values (or a single undefined when not provided).
284
+ * @internal
285
+ */
286
+ normalizeRuleField(value) {
287
+ if (Is.undefined(value)) {
288
+ return [undefined];
289
+ }
290
+ const values = ArrayHelper.fromObjectOrArray(value);
291
+ return Is.arrayValue(values) ? values : [undefined];
292
+ }
293
+ /**
294
+ * Get an existing target state or create an initial state if it doesn't exist.
295
+ * @param targetStates The dictionary of target states.
296
+ * @param target The target key.
297
+ * @returns The target state.
298
+ * @internal
299
+ */
300
+ getOrCreateTargetState(targetStates, target) {
301
+ let state = targetStates[target];
302
+ if (Is.undefined(state)) {
303
+ state = {
304
+ permissionApplies: false,
305
+ prohibitionApplies: false
306
+ };
307
+ targetStates[target] = state;
308
+ }
309
+ return state;
310
+ }
311
+ /**
312
+ * Evaluate whether a prohibition applies.
313
+ * @param policy The policy containing the prohibition.
314
+ * @param agreementAssigner The assigner ID from the agreement.
315
+ * @param agreementAssignee The assignee ID from the agreement.
316
+ * @param prohibition The prohibition to evaluate.
317
+ * @param targetRefinements Additional constraints from target refinement.
318
+ * @param dataSources The operand lookup sources.
319
+ * @param action Optional action to check against the prohibition's applicable actions.
320
+ * @returns True if the prohibition applies.
321
+ * @internal
322
+ */
323
+ async evaluateProhibition(policy, agreementAssigner, agreementAssignee, prohibition, targetRefinements, dataSources, action, decisionTarget) {
324
+ if (!this.isRuleApplicableToParties(prohibition, agreementAssigner, agreementAssignee, dataSources)) {
325
+ return false;
326
+ }
327
+ // Check if the prohibition's action(s) match the requested action
328
+ if (!this.isActionApplicable(prohibition.action, action, dataSources)) {
329
+ return false;
330
+ }
331
+ // ODRL semantics: a rule without constraints is unconditional.
332
+ const constraints = [
333
+ ...ArrayHelper.fromObjectOrArray(prohibition.constraint ?? []),
334
+ ...targetRefinements
335
+ ];
336
+ if (constraints.length > 0 &&
337
+ !constraints.every(c => this.evaluateConstraint(c, dataSources))) {
338
+ return false;
339
+ }
340
+ const prohibitionTargetLookup = this.tryResolveTargetDataSource(this.buildRuleDataContextTargetId(this.getRuleDataContextTargetId(prohibition.target), decisionTarget), dataSources, true);
341
+ const ruleDataContext = prohibitionTargetLookup.value;
342
+ const remedies = ArrayHelper.fromObjectOrArray(prohibition.remedy ?? []);
343
+ if (remedies.length === 0) {
344
+ return true;
345
+ }
346
+ for (const remedy of remedies) {
347
+ if (!(await this.enforceDuty(policy, remedy, dataSources, ruleDataContext))) {
348
+ return true;
349
+ }
350
+ }
351
+ // Remedies satisfied: prohibition is treated as no longer infringed.
352
+ return false;
353
+ }
354
+ /**
355
+ * Evaluate all policy-level obligations.
356
+ * @param agreementAssigner The assigner ID from the agreement.
357
+ * @param agreementAssignee The assignee ID from the agreement.
358
+ * @param policy The policy containing obligations.
359
+ * @param dataSources The operand lookup sources.
360
+ * @returns True if all applicable obligations are fulfilled.
361
+ * @internal
362
+ */
363
+ async evaluatePolicyObligations(agreementAssigner, agreementAssignee, policy, dataSources) {
364
+ const obligations = ArrayHelper.fromObjectOrArray(policy.obligation ?? []);
365
+ for (const obligation of obligations) {
366
+ if (!(await this.evaluateObligation(agreementAssigner, agreementAssignee, policy, obligation, dataSources))) {
367
+ return false;
368
+ }
369
+ }
370
+ return true;
371
+ }
372
+ /**
373
+ * Evaluate whether a policy-level obligation is fulfilled.
374
+ * @param agreementAssigner The assigner ID from the agreement.
375
+ * @param agreementAssignee The assignee ID from the agreement.
376
+ * @param policy The policy containing the obligation.
377
+ * @param obligation The obligation to evaluate.
378
+ * @param dataSources The operand lookup sources.
379
+ * @returns True if the obligation is not applicable or is fulfilled.
380
+ * @internal
381
+ */
382
+ async evaluateObligation(agreementAssigner, agreementAssignee, policy, obligation, dataSources) {
383
+ if (!this.isRuleApplicableToParties(obligation, agreementAssigner, agreementAssignee, dataSources)) {
384
+ return true;
385
+ }
386
+ const { refinements } = this.resolveRuleTarget(obligation, dataSources);
387
+ const obligationTargetLookup = this.tryResolveTargetDataSource(this.getTargetId(obligation.target), dataSources, true);
388
+ const ruleDataContext = obligationTargetLookup.value;
389
+ const constraints = [
390
+ ...ArrayHelper.fromObjectOrArray(obligation.constraint ?? []),
391
+ ...refinements
392
+ ];
393
+ if (constraints.length > 0 &&
394
+ !constraints.every(c => this.evaluateConstraint(c, dataSources))) {
395
+ return true;
396
+ }
397
+ return this.enforceDuty(policy, obligation, dataSources, ruleDataContext);
398
+ }
399
+ /**
400
+ * Merge inherited policies into the current policy.
401
+ * Inherited policies' permissions, prohibitions, and obligations are combined with the current policy's rules.
402
+ * @param policy The policy to evaluate.
403
+ * @returns A new policy with merged rules from all ancestors.
404
+ * @internal
405
+ */
406
+ async mergeInheritedPolicies(policy) {
407
+ const visitedPolicyIds = [];
408
+ visitedPolicyIds.push(OdrlPolicyHelper.getUid(policy) ?? "");
409
+ const inheritedPolicies = await this.resolveInheritedPolicies(policy, visitedPolicyIds, 0);
410
+ const conflictStrategies = new Set();
411
+ if (Is.stringValue(policy.conflict)) {
412
+ conflictStrategies.add(policy.conflict);
413
+ }
414
+ // Start with copies of the current policy's rules
415
+ const mergedPermissions = ArrayHelper.fromObjectOrArray(policy.permission ?? []).map(permission => this.applyPolicyDefaultsToRule(policy, permission));
416
+ const mergedProhibitions = ArrayHelper.fromObjectOrArray(policy.prohibition ?? []).map(prohibition => this.applyPolicyDefaultsToRule(policy, prohibition));
417
+ const mergedObligations = ArrayHelper.fromObjectOrArray(policy.obligation ?? []).map(obligation => this.applyPolicyDefaultsToRule(policy, obligation));
418
+ // Merge rules from each inherited policy, applying the same profile guard as the
419
+ // top-level policy. A parent that declares an unsupported profile may carry rules
420
+ // whose semantics the arbiter cannot guarantee, so it is rejected.
421
+ for (const inheritedPolicy of inheritedPolicies) {
422
+ const inheritedProfiles = ArrayHelper.fromObjectOrArray(inheritedPolicy.profile ?? [])
423
+ .filter(p => Is.stringValue(p))
424
+ .map(p => DefaultPolicyArbiter.normalizeProfileIri(p));
425
+ if (Is.arrayValue(inheritedProfiles) &&
426
+ !inheritedProfiles.every(p => DefaultPolicyArbiter.SUPPORTED_PROFILES.has(p))) {
427
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "inheritedPolicyProfileNotSupported", { policyId: OdrlPolicyHelper.getUid(inheritedPolicy) ?? "" });
428
+ }
429
+ if (Is.stringValue(inheritedPolicy.conflict)) {
430
+ conflictStrategies.add(inheritedPolicy.conflict);
431
+ }
432
+ const inheritedPermissions = ArrayHelper.fromObjectOrArray(inheritedPolicy.permission ?? []);
433
+ if (inheritedPermissions.length > 0) {
434
+ mergedPermissions.push(...inheritedPermissions.map(permission => this.applyPolicyDefaultsToRule(inheritedPolicy, permission)));
435
+ }
436
+ const inheritedProhibitions = ArrayHelper.fromObjectOrArray(inheritedPolicy.prohibition ?? []);
437
+ if (inheritedProhibitions.length > 0) {
438
+ mergedProhibitions.push(...inheritedProhibitions.map(prohibition => this.applyPolicyDefaultsToRule(inheritedPolicy, prohibition)));
439
+ }
440
+ const inheritedObligations = ArrayHelper.fromObjectOrArray(inheritedPolicy.obligation ?? []);
441
+ if (inheritedObligations.length > 0) {
442
+ mergedObligations.push(...inheritedObligations.map(obligation => this.applyPolicyDefaultsToRule(inheritedPolicy, obligation)));
443
+ }
444
+ }
445
+ let mergedConflict;
446
+ if (conflictStrategies.size === 1) {
447
+ mergedConflict = Array.from(conflictStrategies)[0];
448
+ }
449
+ else if (conflictStrategies.size > 1) {
450
+ mergedConflict = OdrlConflictStrategyType.Invalid;
451
+ }
452
+ // Return a new policy with merged rules
453
+ return {
454
+ ...policy,
455
+ conflict: mergedConflict,
456
+ permission: mergedPermissions.length > 0 ? mergedPermissions : undefined,
457
+ prohibition: mergedProhibitions.length > 0 ? mergedProhibitions : undefined,
458
+ obligation: mergedObligations.length > 0 ? mergedObligations : undefined
459
+ };
460
+ }
461
+ /**
462
+ * Resolve inherited policies by their UIDs from the Policy Administration Point.
463
+ * Policies can inherit from other policies via the inheritFrom property.
464
+ * @param policy The policy that may have inheritFrom references.
465
+ * @param visitedPolicyIds Array of policy UIDs already visited in this inheritance chain.
466
+ * @returns Array of inherited policies fetched from the PAP.
467
+ * @internal
468
+ */
469
+ async resolveInheritedPolicies(policy, visitedPolicyIds, currentDepth) {
470
+ const inheritedPolicies = [];
471
+ // If policy has no inheritFrom, return empty array
472
+ if (Is.empty(policy.inheritFrom)) {
473
+ return inheritedPolicies;
474
+ }
475
+ const inheritFromIds = ArrayHelper.fromObjectOrArray(policy.inheritFrom) ?? [];
476
+ for (const inheritFromId of inheritFromIds) {
477
+ const nextDepth = currentDepth + 1;
478
+ if (nextDepth > this._maxInheritanceDepth) {
479
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "maxInheritanceDepthExceeded", {
480
+ policyId: OdrlPolicyHelper.getUid(policy) ?? "",
481
+ inheritFromId,
482
+ maxInheritanceDepth: this._maxInheritanceDepth
483
+ });
484
+ }
485
+ // Check for circular inheritance
486
+ if (visitedPolicyIds.includes(inheritFromId)) {
487
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "circularInheritanceDetected", {
488
+ policyId: OdrlPolicyHelper.getUid(policy) ?? "",
489
+ inheritFromId
490
+ });
491
+ }
492
+ // Fetch the inherited policy from the PAP
493
+ const inheritedPolicy = await this._policyAdministrationPoint.get(inheritFromId);
494
+ if (Is.empty(inheritedPolicy)) {
495
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "inheritedPolicyNotFound", {
496
+ inheritFromId
497
+ });
498
+ }
499
+ // Mark this policy as visited
500
+ visitedPolicyIds.push(inheritFromId);
501
+ inheritedPolicies.push(inheritedPolicy);
502
+ // Recursively resolve inherited policies of the parent
503
+ const grandparentPolicies = await this.resolveInheritedPolicies(inheritedPolicy, visitedPolicyIds, nextDepth);
504
+ inheritedPolicies.push(...grandparentPolicies);
505
+ }
506
+ return inheritedPolicies;
507
+ }
508
+ /**
509
+ * Apply policy-level default values (assigner, assignee, target, action) to rules that don't override them.
510
+ * @param policy The policy providing defaults.
511
+ * @param rule The rule to apply defaults to.
512
+ * @returns The rule with policy-level defaults applied.
513
+ * @internal
514
+ */
515
+ applyPolicyDefaultsToRule(policy, rule) {
516
+ const assigner = Is.empty(rule.assigner) ? policy.assigner : rule.assigner;
517
+ const assignee = Is.empty(rule.assignee) ? policy.assignee : rule.assignee;
518
+ const target = Is.empty(rule.target) ? policy.target : rule.target;
519
+ const action = Is.empty(rule.action) ? policy.action : rule.action;
520
+ const assignerIds = OdrlPolicyHelper.getPartyIds(assigner);
521
+ const ruleAssignerIds = OdrlPolicyHelper.getPartyIds(rule.assigner);
522
+ const assigneeIds = OdrlPolicyHelper.getPartyIds(assignee);
523
+ const ruleAssigneeIds = OdrlPolicyHelper.getPartyIds(rule.assignee);
524
+ let assignerEqual = false;
525
+ if (Is.empty(assignerIds) && Is.empty(ruleAssignerIds)) {
526
+ assignerEqual = true;
527
+ }
528
+ else if (!Is.empty(assignerIds) && !Is.empty(ruleAssignerIds)) {
529
+ assignerEqual =
530
+ assignerIds.length === ruleAssignerIds.length &&
531
+ assignerIds.every(id => ruleAssignerIds.includes(id));
532
+ }
533
+ let assigneeEqual = false;
534
+ if (Is.empty(assigneeIds) && Is.empty(ruleAssigneeIds)) {
535
+ assigneeEqual = true;
536
+ }
537
+ else if (!Is.empty(assigneeIds) && !Is.empty(ruleAssigneeIds)) {
538
+ assigneeEqual =
539
+ assigneeIds.length === ruleAssigneeIds.length &&
540
+ assigneeIds.every(id => ruleAssigneeIds.includes(id));
541
+ }
542
+ const targetEqual = target === rule.target;
543
+ const actionEqual = action === rule.action;
544
+ if (assignerEqual && assigneeEqual && targetEqual && actionEqual) {
545
+ return rule;
546
+ }
547
+ return {
548
+ ...rule,
549
+ assigner,
550
+ assignee,
551
+ target,
552
+ action
553
+ };
554
+ }
555
+ /**
556
+ * Determine whether a rule applies to the agreement parties based on assigner/assignee.
557
+ * @param rule The rule to evaluate.
558
+ * @param agreementAssigner The assigner ID from the agreement.
559
+ * @param agreementAssignee The assignee ID from the agreement.
560
+ * @returns True if the rule is applicable to the agreement parties.
561
+ * @internal
562
+ */
563
+ isRuleApplicableToParties(rule, agreementAssigner, agreementAssignee, dataSources) {
564
+ const assignerContext = this.resolveRulePartyContext(rule.assigner);
565
+ const assigneeContext = this.resolveRulePartyContext(rule.assignee);
566
+ if (!this.isPartyApplicable(assignerContext.partyIds, agreementAssigner)) {
567
+ return false;
568
+ }
569
+ if (assignerContext.refinements.length > 0 &&
570
+ !assignerContext.refinements.every(c => this.evaluateConstraint(c, dataSources))) {
571
+ return false;
572
+ }
573
+ if (!this.isPartyApplicable(assigneeContext.partyIds, agreementAssignee)) {
574
+ return false;
575
+ }
576
+ if (assigneeContext.refinements.length > 0 &&
577
+ !assigneeContext.refinements.every(c => this.evaluateConstraint(c, dataSources))) {
578
+ return false;
579
+ }
580
+ return true;
581
+ }
582
+ /**
583
+ * Resolve rule party identifiers and refinements.
584
+ * PartyCollection source values are currently not supported for party matching.
585
+ * @param party The rule party value.
586
+ * @returns Resolved party identifiers and refinement constraints.
587
+ * @throws GeneralError if PartyCollection source has a value.
588
+ * @internal
589
+ */
590
+ resolveRulePartyContext(party) {
591
+ const partyIds = [];
592
+ const refinements = [];
593
+ const parties = ArrayHelper.fromObjectOrArray(party ?? []);
594
+ for (const partyEntry of parties) {
595
+ if (Is.stringValue(partyEntry)) {
596
+ partyIds.push(partyEntry);
597
+ }
598
+ else if (Is.object(partyEntry)) {
599
+ // Guard against unsupported ODRL party properties
600
+ if (Is.notEmpty(partyEntry.assignerOf)) {
601
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyAssignerOfNotSupported");
602
+ }
603
+ if (Is.notEmpty(partyEntry.assigneeOf)) {
604
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyAssigneeOfNotSupported");
605
+ }
606
+ if (Is.notEmpty(partyEntry.partOf)) {
607
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyPartOfNotSupported");
608
+ }
609
+ if (OdrlPolicyHelper.getType(partyEntry) === OdrlTypes.PartyCollection) {
610
+ const partyCollectionEntry = partyEntry;
611
+ if (Is.stringValue(partyCollectionEntry.source)) {
612
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "partyCollectionSourceNotSupported", {
613
+ source: partyCollectionEntry.source ?? ""
614
+ });
615
+ }
616
+ refinements.push(...ArrayHelper.fromObjectOrArray(partyCollectionEntry.refinement ?? []));
617
+ }
618
+ else {
619
+ const partyId = OdrlPolicyHelper.getUid(partyEntry);
620
+ if (Is.stringValue(partyId)) {
621
+ partyIds.push(partyId);
622
+ }
623
+ }
624
+ }
625
+ }
626
+ return {
627
+ partyIds,
628
+ refinements
629
+ };
630
+ }
631
+ /**
632
+ * Determine whether a rule party constraint applies to the agreement parties.
633
+ * Rule party is treated as a constraint: if specified, it must match at least one agreement party.
634
+ * @param rulePartyIds The party ids specified on the rule (if any).
635
+ * @param agreementPartyIds The party ids extracted from the agreement.
636
+ * @returns True if the rule party constraint is satisfied.
637
+ * @internal
638
+ */
639
+ isPartyApplicable(rulePartyIds, agreementPartyIds) {
640
+ // No party specified on rule means it applies to any agreement party.
641
+ if (Is.empty(rulePartyIds)) {
642
+ return true;
643
+ }
644
+ // Rule specifies party/parties, but agreement doesn't provide any.
645
+ if (Is.empty(agreementPartyIds)) {
646
+ return false;
647
+ }
648
+ for (const rulePartyId of rulePartyIds) {
649
+ if (agreementPartyIds.includes(rulePartyId)) {
650
+ return true;
651
+ }
652
+ }
653
+ return false;
654
+ }
655
+ /**
656
+ * Determine whether a rule's action(s) match the requested action.
657
+ * Supports exact match, includedIn hierarchy, and implies relationships.
658
+ * If no action is specified in the rule, it applies to all actions (action-agnostic).
659
+ * If an action is specified in the rule and a specific action is requested, they must match.
660
+ * If an action is specified in the rule but no specific action is requested, the rule applies (general evaluation).
661
+ * @param ruleActions The actions defined in the rule (can be string, object, or array).
662
+ * @param requestedAction The action being requested (optional).
663
+ * @returns True if the rule's action(s) apply.
664
+ * @internal
665
+ */
666
+ isActionApplicable(ruleActions, requestedAction, dataSources) {
667
+ // If the rule has no action specified, it applies to all actions
668
+ if (Is.empty(ruleActions)) {
669
+ return true;
670
+ }
671
+ // If the rule has actions but no specific action is requested,
672
+ // the rule applies (we're evaluating permissions in general)
673
+ if (Is.empty(requestedAction)) {
674
+ return true;
675
+ }
676
+ const requestedActionId = Is.string(requestedAction)
677
+ ? requestedAction
678
+ : OdrlPolicyHelper.getUid(requestedAction);
679
+ if (!Is.stringValue(requestedActionId)) {
680
+ return false;
681
+ }
682
+ const ruleActionArray = ArrayHelper.fromObjectOrArray(ruleActions) ?? [];
683
+ for (const ruleAction of ruleActionArray) {
684
+ if (this.ruleActionCoversRequested(ruleAction, requestedActionId, dataSources)) {
685
+ return true;
686
+ }
687
+ }
688
+ return false;
689
+ }
690
+ /**
691
+ * Determine whether a single rule action covers a requested action.
692
+ * Covers exact match plus ODRL action hierarchy semantics.
693
+ * - includedIn: the rule action is a sub-action of a broader parent.
694
+ * A rule naming the narrower action also covers requests for the parent.
695
+ * E.g. rule action "print" with includedIn "reproduce" covers a request for "reproduce".
696
+ * - implies: the rule action entails another action.
697
+ * A rule granting action X also covers action Y when X implies Y.
698
+ * E.g. rule action "distribute" implying "reproduce" covers a request for "reproduce".
699
+ * @param ruleAction The action specified in the rule.
700
+ * @param requestedActionId The requested action identifier.
701
+ * @returns True if the rule action covers the requested action.
702
+ * @internal
703
+ */
704
+ ruleActionCoversRequested(ruleAction, requestedActionId, dataSources) {
705
+ // Extract the rule action ID — support both @id and rdf:value forms
706
+ let ruleActionId;
707
+ if (Is.string(ruleAction)) {
708
+ ruleActionId = ruleAction;
709
+ }
710
+ else if (Is.object(ruleAction)) {
711
+ ruleActionId = ruleAction["rdf:value"]?.["@id"] ?? OdrlPolicyHelper.getUid(ruleAction);
712
+ }
713
+ // Determine whether this rule action covers the requested action via any semantic path.
714
+ let covers = false;
715
+ if (Is.stringValue(ruleActionId) && ruleActionId === requestedActionId) {
716
+ // Exact match
717
+ covers = true;
718
+ }
719
+ else if (Is.object(ruleAction)) {
720
+ // includedIn: rule action A includedIn B means A is a sub-type of B.
721
+ // A rule that names the narrower action A with includedIn B also covers requests for B.
722
+ if (Is.stringValue(ruleAction.includedIn) && ruleAction.includedIn === requestedActionId) {
723
+ covers = true;
724
+ }
725
+ // implies: rule action A implies B means exercising A also entails B.
726
+ // A rule granting A therefore also grants each implied action.
727
+ if (!covers && (ruleAction.implies ?? []).includes(requestedActionId)) {
728
+ covers = true;
729
+ }
730
+ }
731
+ if (!covers) {
732
+ return false;
733
+ }
734
+ // If the action specifies refinements, all must be satisfied for the action to apply.
735
+ // Refinements constrain the manner in which the action is exercised (e.g. print count <= 5).
736
+ if (Is.object(ruleAction) && Is.notEmpty(ruleAction.refinement)) {
737
+ const refinements = ArrayHelper.fromObjectOrArray(ruleAction.refinement ?? []);
738
+ return refinements.every((refinement) => this.evaluateConstraint(refinement, dataSources));
739
+ }
740
+ return true;
741
+ }
742
+ /**
743
+ * Apply a permission and create decisions based on that information.
744
+ * @param agreementAssigner The assigner ID from the agreement.
745
+ * @param agreementAssignee The assignee ID from the agreement.
746
+ * @param policy The policy containing the permission.
747
+ * @param permission The permission to apply.
748
+ * @param targetRefinements Additional constraints from target refinement.
749
+ * @param dataSources The operand lookup sources.
750
+ * @returns True if the permission applies.
751
+ * @internal
752
+ */
753
+ async evaluatePermission(agreementAssigner, agreementAssignee, policy, permission, targetRefinements, dataSources, action, decisionTarget) {
754
+ if (!this.isRuleApplicableToParties(permission, agreementAssigner, agreementAssignee, dataSources)) {
755
+ return false;
756
+ }
757
+ // Check if the permission's action(s) match the requested action
758
+ if (!this.isActionApplicable(permission.action, action, dataSources)) {
759
+ return false;
760
+ }
761
+ // ODRL semantics: a Permission without constraints is unconditional.
762
+ const constraints = [
763
+ ...ArrayHelper.fromObjectOrArray(permission.constraint ?? []),
764
+ ...targetRefinements
765
+ ];
766
+ const permissionTargetLookup = this.tryResolveTargetDataSource(this.buildRuleDataContextTargetId(this.getRuleDataContextTargetId(permission.target), decisionTarget), dataSources, true);
767
+ const ruleDataContext = permissionTargetLookup.value;
768
+ if (constraints.length === 0) {
769
+ return this.enforcePermissionDuties(policy, permission, dataSources, ruleDataContext);
770
+ }
771
+ // All constraints must be satisfied for the permission to apply.
772
+ const constraintsSatisfied = constraints.every(c => this.evaluateConstraint(c, dataSources));
773
+ if (!constraintsSatisfied) {
774
+ return false;
775
+ }
776
+ return this.enforcePermissionDuties(policy, permission, dataSources, ruleDataContext);
777
+ }
778
+ /**
779
+ * Enforce duties attached to a permission.
780
+ * @param policy The policy being evaluated.
781
+ * @param permission The permission being evaluated.
782
+ * @param dataSources The operand lookup sources.
783
+ * @param ruleDataContext The target-scoped data context passed to enforcers.
784
+ * @returns True if all duties are enforced or none are present.
785
+ * @internal
786
+ */
787
+ async enforcePermissionDuties(policy, permission, dataSources, ruleDataContext) {
788
+ const duties = ArrayHelper.fromObjectOrArray(permission.duty ?? []);
789
+ if (duties.length === 0) {
790
+ return true;
791
+ }
792
+ for (const duty of duties) {
793
+ const enforced = await this.enforceDuty(policy, duty, dataSources, ruleDataContext);
794
+ if (!enforced) {
795
+ return false;
796
+ }
797
+ }
798
+ return true;
799
+ }
800
+ /**
801
+ * Enforce a single duty using registered obligation enforcers.
802
+ * @param policy The policy being evaluated.
803
+ * @param duty The duty to enforce.
804
+ * @param dataSources The operand lookup sources.
805
+ * @param ruleDataContext The target-scoped data context passed to enforcers.
806
+ * @returns True if any enforcer succeeds.
807
+ * @internal
808
+ */
809
+ async enforceDuty(policy, duty, dataSources, ruleDataContext) {
810
+ const enforcerNames = PolicyObligationEnforcerFactory.names();
811
+ const information = dataSources[DefaultPolicyArbiter._INFORMATION_SOURCE_KEY];
812
+ if (enforcerNames.length === 0) {
813
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "noObligationEnforcersRegistered");
814
+ }
815
+ for (const enforcerName of enforcerNames) {
816
+ const enforcer = PolicyObligationEnforcerFactory.get(enforcerName);
817
+ if (await enforcer.enforce(policy, duty, information, ruleDataContext)) {
818
+ return true;
819
+ }
820
+ }
821
+ const consequences = ArrayHelper.fromObjectOrArray(duty.consequence ?? []);
822
+ if (consequences.length === 0) {
823
+ return false;
824
+ }
825
+ for (const consequence of consequences) {
826
+ if (!(await this.enforceDuty(policy, consequence, dataSources, ruleDataContext))) {
827
+ return false;
828
+ }
829
+ }
830
+ return true;
831
+ }
832
+ /**
833
+ * Resolve a target string to a matching datasource prefix and remaining target value.
834
+ * @param targetId The target identifier to resolve.
835
+ * @param dataSources The available lookup sources.
836
+ * @param resolveValue True to resolve an item from the target path/key.
837
+ * @returns The matching prefix, source, remaining target and optional resolved value.
838
+ * @internal
839
+ */
840
+ tryResolveTargetDataSource(targetId, dataSources, resolveValue = false) {
841
+ // If there is no target id, default to the data datasource
842
+ if (Is.empty(targetId)) {
843
+ return {
844
+ source: dataSources[DefaultPolicyArbiter._DATA_SOURCE_KEY],
845
+ target: "$",
846
+ value: dataSources[DefaultPolicyArbiter._DATA_SOURCE_KEY]
847
+ };
848
+ }
849
+ // Handle twin:jsonPath:<datasource>:<expression> format.
850
+ // The datasource segment is optional; when absent the expression starts with "$"
851
+ // and falls back to the primary data source.
852
+ if (targetId.startsWith(`${DefaultPolicyArbiter._TWIN_JSONPATH}:`)) {
853
+ const rest = targetId.slice(DefaultPolicyArbiter._TWIN_JSONPATH.length + 1);
854
+ const matchingKey = Object.keys(dataSources).find(k => rest.startsWith(`${k}:`));
855
+ const sourceKey = matchingKey ?? DefaultPolicyArbiter._DATA_SOURCE_KEY;
856
+ const expression = matchingKey ? rest.slice(matchingKey.length + 1) : rest;
857
+ return this.resolveDataSourceByKey(sourceKey, expression, dataSources, resolveValue);
858
+ }
859
+ // Handle <datasource>:<expression> format
860
+ const matchingKey = Object.keys(dataSources).find(k => targetId.startsWith(`${k}:`));
861
+ if (matchingKey) {
862
+ const expression = targetId.slice(matchingKey.length + 1);
863
+ return this.resolveDataSourceByKey(matchingKey, expression, dataSources, resolveValue);
864
+ }
865
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
866
+ target: targetId
867
+ });
868
+ }
869
+ /**
870
+ * Resolve a datasource by key and evaluate a JSONPath expression against it.
871
+ * @param sourceKey The datasource key.
872
+ * @param expression The JSONPath expression.
873
+ * @param dataSources The available datasources.
874
+ * @param resolveValue Whether to evaluate the expression and return the matched value.
875
+ * @returns The resolved source, target expression, and optionally the matched value.
876
+ * @internal
877
+ */
878
+ resolveDataSourceByKey(sourceKey, expression, dataSources, resolveValue = false) {
879
+ if (!Is.stringValue(expression)) {
880
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
881
+ operand: "target"
882
+ });
883
+ }
884
+ if (!expression.startsWith("$")) {
885
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
886
+ target: `${sourceKey}:${expression}`
887
+ });
888
+ }
889
+ const source = dataSources[sourceKey];
890
+ if (!resolveValue) {
891
+ return { source, target: expression };
892
+ }
893
+ const matches = JsonPathHelper.query(expression, source);
894
+ if (matches.length === 0) {
895
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
896
+ target: `${sourceKey}:${expression}`
897
+ });
898
+ }
899
+ return {
900
+ source,
901
+ target: expression,
902
+ value: matches.length === 1 ? matches[0].value : matches.map(m => m.value)
903
+ };
904
+ }
905
+ /**
906
+ * Extract the target id from the permission.
907
+ * @param target The permission target.
908
+ * @returns The information key, or undefined when the target is not an information reference.
909
+ * @internal
910
+ */
911
+ getTargetId(target) {
912
+ if (Is.undefined(target)) {
913
+ return undefined;
914
+ }
915
+ if (Is.array(target) && target.length > 1) {
916
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "multipleTargetsNotSupported");
917
+ }
918
+ const arr = ArrayHelper.fromObjectOrArray(target ?? []);
919
+ if (arr.length === 0) {
920
+ return undefined;
921
+ }
922
+ const first = arr[0];
923
+ if (Is.string(first)) {
924
+ return first;
925
+ }
926
+ if (this.isTwinJsonPathTarget(first)) {
927
+ const t = first;
928
+ const expression = t[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
929
+ if (!Is.stringValue(expression)) {
930
+ return undefined;
931
+ }
932
+ const dataSource = t[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE];
933
+ const sourceKey = Is.stringValue(dataSource)
934
+ ? dataSource
935
+ : DefaultPolicyArbiter._DATA_SOURCE_KEY;
936
+ return `${sourceKey}:${expression}`;
937
+ }
938
+ return OdrlPolicyHelper.getUid(first);
939
+ }
940
+ /**
941
+ * Resolve the target identifier used for rule data context lookup.
942
+ * For AssetCollection targets the `source` property is used because the collection has no `uid`.
943
+ * Falls back to `getTargetId` for all other target forms.
944
+ * @param target The rule target field value.
945
+ * @returns The prefixed target string for data context resolution.
946
+ * @internal
947
+ */
948
+ getRuleDataContextTargetId(target) {
949
+ const arr = ArrayHelper.fromObjectOrArray(target ?? []);
950
+ if (arr.length === 1) {
951
+ const firstTarget = arr[0];
952
+ if (Is.object(firstTarget) &&
953
+ OdrlPolicyHelper.getType(firstTarget) === OdrlTypes.AssetCollection &&
954
+ firstTarget.source === DefaultPolicyArbiter._TWIN_JSONPATH) {
955
+ const ctx = firstTarget;
956
+ const expression = ctx[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
957
+ if (Is.stringValue(expression)) {
958
+ const dataSource = ctx[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE];
959
+ const sourceKey = Is.stringValue(dataSource)
960
+ ? dataSource
961
+ : DefaultPolicyArbiter._DATA_SOURCE_KEY;
962
+ return `${DefaultPolicyArbiter._TWIN_JSONPATH}:${sourceKey}:${expression}`;
963
+ }
964
+ }
965
+ }
966
+ return this.getTargetId(target);
967
+ }
968
+ /**
969
+ * Build a concrete prefixed target id for rule data-context lookup.
970
+ * @param baseTargetId The original prefixed target id from the rule.
971
+ * @param decisionTarget The concrete decision JSONPath target.
972
+ * @returns The concrete prefixed target id.
973
+ * @internal
974
+ */
975
+ buildRuleDataContextTargetId(baseTargetId, decisionTarget) {
976
+ if (!Is.stringValue(decisionTarget) || decisionTarget === "$") {
977
+ return baseTargetId;
978
+ }
979
+ if (!Is.stringValue(baseTargetId)) {
980
+ return undefined;
981
+ }
982
+ const pathStartIndex = baseTargetId.indexOf(":$");
983
+ if (pathStartIndex < 0) {
984
+ return baseTargetId;
985
+ }
986
+ return `${baseTargetId.slice(0, pathStartIndex)}:${decisionTarget}`;
987
+ }
988
+ /**
989
+ * Resolve a rule target into a policy-decision JSONPath target and extracted refinements.
990
+ * For AssetCollection targets, `source` is treated as the decision target and `refinement`
991
+ * constraints are applied as additional rule constraints.
992
+ * @param rule The rule to resolve the target for.
993
+ * @returns The decision target and target refinements.
994
+ * @throws GeneralError if target is invalid or unsupported.
995
+ * @internal
996
+ */
997
+ resolveRuleTarget(rule, dataSources) {
998
+ const arr = ArrayHelper.fromObjectOrArray(rule.target ?? []);
999
+ if (arr.length === 0) {
1000
+ return {
1001
+ target: "$",
1002
+ refinements: []
1003
+ };
1004
+ }
1005
+ if (arr.length > 1) {
1006
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "multipleTargetsNotSupported");
1007
+ }
1008
+ const firstTarget = arr[0];
1009
+ if (this.isTwinJsonPathTarget(firstTarget)) {
1010
+ const t = firstTarget;
1011
+ const resolved = this.resolveDataSourceByKey(t[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE] ?? DefaultPolicyArbiter._DATA_SOURCE_KEY, t[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION], dataSources);
1012
+ return {
1013
+ target: resolved.target,
1014
+ refinements: []
1015
+ };
1016
+ }
1017
+ if (Is.object(firstTarget)) {
1018
+ // Guard against unsupported ODRL asset properties
1019
+ if (Is.notEmpty(firstTarget.hasPolicy)) {
1020
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetHasPolicyNotSupported");
1021
+ }
1022
+ if (Is.notEmpty(firstTarget.partOf)) {
1023
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetPartOfNotSupported");
1024
+ }
1025
+ if (Is.object(firstTarget) &&
1026
+ OdrlPolicyHelper.getType(firstTarget) === OdrlTypes.AssetCollection) {
1027
+ if (firstTarget.source !== DefaultPolicyArbiter._TWIN_JSONPATH) {
1028
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetCollectionSourceNotSupported", {
1029
+ source: firstTarget.source ?? ""
1030
+ });
1031
+ }
1032
+ const ctx = firstTarget;
1033
+ const dataSource = ctx[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE];
1034
+ const sourceKey = Is.stringValue(dataSource)
1035
+ ? dataSource
1036
+ : DefaultPolicyArbiter._DATA_SOURCE_KEY;
1037
+ let sourceLookup;
1038
+ try {
1039
+ sourceLookup = this.resolveDataSourceByKey(sourceKey, ctx[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION], dataSources);
1040
+ }
1041
+ catch {
1042
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetCollectionSourceNotSupported", {
1043
+ source: ctx[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION] ?? ""
1044
+ });
1045
+ }
1046
+ return {
1047
+ target: sourceLookup.target,
1048
+ refinements: ArrayHelper.fromObjectOrArray(firstTarget.refinement ?? [])
1049
+ };
1050
+ }
1051
+ }
1052
+ const targetId = Is.string(firstTarget) ? firstTarget : OdrlPolicyHelper.getUid(firstTarget);
1053
+ if (!Is.stringValue(targetId)) {
1054
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
1055
+ target: ""
1056
+ });
1057
+ }
1058
+ const targetLookup = this.tryResolveTargetDataSource(targetId, dataSources);
1059
+ return {
1060
+ target: targetLookup.target,
1061
+ refinements: []
1062
+ };
1063
+ }
1064
+ /**
1065
+ * Resolve decision targets for a rule.
1066
+ * AssetCollection wildcard targets with refinements are expanded to per-item targets.
1067
+ * @param rule The rule being evaluated.
1068
+ * @param dataSources The operand lookup sources.
1069
+ * @returns The decision targets and scoped refinements.
1070
+ * @internal
1071
+ */
1072
+ resolveRuleDecisionTargets(rule, dataSources) {
1073
+ const resolvedTarget = this.resolveRuleTarget(rule, dataSources);
1074
+ if (!this.shouldExpandToPerItemTargets(rule, resolvedTarget)) {
1075
+ return [resolvedTarget];
1076
+ }
1077
+ const sourceLookup = this.tryResolveTargetDataSource(this.getRuleDataContextTargetId(rule.target), dataSources);
1078
+ const matches = JsonPathHelper.query(sourceLookup.target, sourceLookup.source);
1079
+ if (matches.length === 0) {
1080
+ return [resolvedTarget];
1081
+ }
1082
+ return matches.map(match => {
1083
+ const itemTarget = this.normalizeDecisionTargetPath(match.path ?? resolvedTarget.target);
1084
+ return {
1085
+ target: itemTarget,
1086
+ refinements: resolvedTarget.refinements.map(refinement => this.rewriteRefinementForDecisionTarget(refinement, resolvedTarget.target, itemTarget))
1087
+ };
1088
+ });
1089
+ }
1090
+ /**
1091
+ * Determine if a rule should be expanded to per-item targets.
1092
+ * @param rule The rule.
1093
+ * @param resolvedTarget The resolved target details.
1094
+ * @returns True if the rule should emit per-item decisions.
1095
+ * @internal
1096
+ */
1097
+ shouldExpandToPerItemTargets(rule, resolvedTarget) {
1098
+ if (resolvedTarget.refinements.length === 0 || !resolvedTarget.target.includes("[*]")) {
1099
+ return false;
1100
+ }
1101
+ const targets = ArrayHelper.fromObjectOrArray(rule.target ?? []);
1102
+ if (targets.length !== 1 || !Is.object(targets[0])) {
1103
+ return false;
1104
+ }
1105
+ return OdrlPolicyHelper.getType(targets[0]) === OdrlTypes.AssetCollection;
1106
+ }
1107
+ /**
1108
+ * Rewrite a refinement so wildcard paths are scoped to a concrete item target.
1109
+ * @param refinement The refinement to rewrite.
1110
+ * @param sourceTarget The wildcard source target.
1111
+ * @param itemTarget The concrete item target.
1112
+ * @returns The rewritten refinement.
1113
+ * @internal
1114
+ */
1115
+ rewriteRefinementForDecisionTarget(refinement, sourceTarget, itemTarget) {
1116
+ const logicalConstraint = this.getLogicalConstraintOperands(refinement);
1117
+ if (logicalConstraint) {
1118
+ return {
1119
+ ...refinement,
1120
+ [logicalConstraint.operator]: logicalConstraint.constraints.map(item => this.rewriteRefinementForDecisionTarget(item, sourceTarget, itemTarget))
1121
+ };
1122
+ }
1123
+ const regularConstraint = refinement;
1124
+ const constraintWithExtensions = regularConstraint;
1125
+ const canonicalExpression = constraintWithExtensions[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
1126
+ const rewrittenConstraint = {
1127
+ ...regularConstraint,
1128
+ leftOperand: this.rewriteOperandForDecisionTarget(regularConstraint.leftOperand, sourceTarget, itemTarget),
1129
+ rightOperand: this.rewriteOperandForDecisionTarget(regularConstraint.rightOperand, sourceTarget, itemTarget)
1130
+ };
1131
+ if (Is.stringValue(canonicalExpression)) {
1132
+ rewrittenConstraint[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION] =
1133
+ this.rewriteWildcardPath(canonicalExpression, sourceTarget, itemTarget);
1134
+ }
1135
+ return rewrittenConstraint;
1136
+ }
1137
+ /**
1138
+ * Rewrite JSONPath-based operands from wildcard source to concrete item target.
1139
+ * @param operand The operand to rewrite.
1140
+ * @param sourceTarget The wildcard source target.
1141
+ * @param itemTarget The concrete item target.
1142
+ * @returns The rewritten operand.
1143
+ * @internal
1144
+ */
1145
+ rewriteOperandForDecisionTarget(operand, sourceTarget, itemTarget) {
1146
+ if (Is.object(operand)) {
1147
+ const typedOperand = {
1148
+ ...operand
1149
+ };
1150
+ if (this.isTwinJsonPathOperandType(typedOperand["@type"])) {
1151
+ if (Is.stringValue(typedOperand["@value"])) {
1152
+ typedOperand["@value"] = this.rewriteWildcardPath(typedOperand["@value"], sourceTarget, itemTarget);
1153
+ }
1154
+ const canonicalExpression = typedOperand[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
1155
+ if (Is.stringValue(canonicalExpression)) {
1156
+ typedOperand[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION] = this.rewriteWildcardPath(canonicalExpression, sourceTarget, itemTarget);
1157
+ }
1158
+ }
1159
+ return typedOperand;
1160
+ }
1161
+ // Legacy string form: "twin:information:$.foo". Extract the prefix, rewrite the
1162
+ // JSONPath expression to scope to the per-item target, then re-assemble. Without
1163
+ // this, the leftOperand stays at the wildcard
1164
+ // `$.itemList.itemListElement[*].unloadingLocation.id` for every item, returning
1165
+ // the full array of all ids per check — so all items pass the equality test.
1166
+ if (Is.stringValue(operand) &&
1167
+ operand.startsWith(`twin:${DefaultPolicyArbiter._INFORMATION_SOURCE_KEY}:`)) {
1168
+ const prefix = `twin:${DefaultPolicyArbiter._INFORMATION_SOURCE_KEY}:`;
1169
+ const expression = operand.slice(prefix.length);
1170
+ const rewritten = this.rewriteWildcardPath(expression, sourceTarget, itemTarget);
1171
+ return `${prefix}${rewritten}`;
1172
+ }
1173
+ return operand;
1174
+ }
1175
+ /**
1176
+ * Rewrite wildcard source JSONPath segments to a concrete item JSONPath.
1177
+ * @param valuePath The operand path.
1178
+ * @param sourceTarget The wildcard source path.
1179
+ * @param itemTarget The concrete item path.
1180
+ * @returns The rewritten path.
1181
+ * @internal
1182
+ */
1183
+ rewriteWildcardPath(valuePath, sourceTarget, itemTarget) {
1184
+ if (!sourceTarget.includes("[*]") || !valuePath.includes("[*]")) {
1185
+ return valuePath;
1186
+ }
1187
+ if (valuePath.startsWith(sourceTarget)) {
1188
+ return `${itemTarget}${valuePath.slice(sourceTarget.length)}`;
1189
+ }
1190
+ return valuePath;
1191
+ }
1192
+ /**
1193
+ * Normalize JSONPath strings to dot notation for stable decision targets.
1194
+ * @param path The JSONPath to normalize.
1195
+ * @returns The normalized path.
1196
+ * @internal
1197
+ */
1198
+ normalizeDecisionTargetPath(path) {
1199
+ return path.replace(/\['([^']+)']/g, ".$1");
1200
+ }
1201
+ /**
1202
+ * Evaluate a single ODRL constraint against the available context.
1203
+ * Supports logical constraint composition through nested refinements.
1204
+ * @param constraint The constraint to evaluate.
1205
+ * @param dataSources The operand lookup sources.
1206
+ * @returns True if the constraint is satisfied.
1207
+ * @internal
1208
+ */
1209
+ evaluateConstraint(constraint, dataSources) {
1210
+ const logicalConstraint = this.getLogicalConstraintOperands(constraint);
1211
+ if (logicalConstraint) {
1212
+ return this.evaluateLogicalConstraint(logicalConstraint, dataSources);
1213
+ }
1214
+ // Must be a regular constraint beyond this point
1215
+ const regularConstraint = constraint;
1216
+ // rightOperandReference is not supported — it requires an external IRI lookup that
1217
+ // is outside the scope of the local evaluation engine.
1218
+ if (Is.notEmpty(regularConstraint.rightOperandReference)) {
1219
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "rightOperandReferenceNotSupported");
1220
+ }
1221
+ // dataType specifies how the rightOperand value should be coerced before comparison.
1222
+ // Without dataType-aware coercion logic the comparison may produce incorrect results.
1223
+ if (Is.notEmpty(regularConstraint.dataType)) {
1224
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintDataTypeNotSupported");
1225
+ }
1226
+ // unit specifies the measurement unit for the right operand (e.g. currency, length).
1227
+ // Unit-aware comparison is not implemented.
1228
+ if (Is.notEmpty(regularConstraint.unit)) {
1229
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintUnitNotSupported");
1230
+ }
1231
+ // status represents a state-based evaluation operand (e.g. odrl:policyUsage).
1232
+ // State-based evaluation is not implemented.
1233
+ if (Is.notEmpty(regularConstraint.status)) {
1234
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintStatusNotSupported");
1235
+ }
1236
+ // Evaluate the main constraint condition
1237
+ const leftValue = this.calculateOperandValue(regularConstraint.leftOperand, dataSources, regularConstraint);
1238
+ const rightValue = this.calculateOperandValue(regularConstraint.rightOperand, dataSources, regularConstraint);
1239
+ const mainSatisfied = this.evaluateOperator(regularConstraint.operator, leftValue, rightValue);
1240
+ // If main constraint is not satisfied, the overall constraint fails
1241
+ return mainSatisfied;
1242
+ }
1243
+ /**
1244
+ * Extract logical constraint operands when present.
1245
+ * @param constraint The constraint to inspect.
1246
+ * @returns The logical operator and its operands, or undefined when not logical.
1247
+ * @throws GeneralError if logical constraint operands are not unique.
1248
+ * @internal
1249
+ */
1250
+ getLogicalConstraintOperands(constraint) {
1251
+ const logicalConstraint = constraint;
1252
+ const operators = Object.values(OdrlLogicalConstraintType);
1253
+ for (const operator of operators) {
1254
+ const value = logicalConstraint[operator];
1255
+ if (!Is.undefined(value)) {
1256
+ const constraints = this.normalizeLogicalConstraintOperands(value);
1257
+ this.validateLogicalConstraintOperandUniqueness(constraints, operator);
1258
+ return {
1259
+ operator,
1260
+ constraints
1261
+ };
1262
+ }
1263
+ }
1264
+ return undefined;
1265
+ }
1266
+ /**
1267
+ * Normalize logical constraint operands into a constraint array.
1268
+ * Handles both IOdrlConstraint and IOdrlLogicalConstraint types.
1269
+ * @param raw The raw operand value.
1270
+ * @returns The constraint array.
1271
+ * @internal
1272
+ */
1273
+ normalizeLogicalConstraintOperands(raw) {
1274
+ let normalized = raw;
1275
+ if (Is.object(normalized) &&
1276
+ !Is.undefined(normalized["@list"])) {
1277
+ normalized = normalized["@list"];
1278
+ }
1279
+ return (ArrayHelper.fromObjectOrArray(normalized) ?? [])
1280
+ .filter(item => Is.object(item))
1281
+ .map(item => item);
1282
+ }
1283
+ /**
1284
+ * Validate that all operands in a logical constraint are unique.
1285
+ * ODRL spec 2.5.2 requires that all operand values MUST be unique Constraint instances.
1286
+ * Uniqueness is checked by uid property and id property.
1287
+ * @param constraints The constraint operands to validate.
1288
+ * @param operator The logical operator type (for error messaging).
1289
+ * @throws GeneralError if duplicate constraints are found.
1290
+ * @internal
1291
+ */
1292
+ validateLogicalConstraintOperandUniqueness(constraints, operator) {
1293
+ const seenIdentifiers = new Set();
1294
+ for (let i = 0; i < constraints.length; i++) {
1295
+ const constraint = constraints[i];
1296
+ const identifier = OdrlPolicyHelper.getUid(constraint);
1297
+ // If we have an identifier, check for duplicates
1298
+ if (Is.stringValue(identifier)) {
1299
+ if (seenIdentifiers.has(identifier)) {
1300
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "logicalConstraintOperandNotUnique", {
1301
+ operator,
1302
+ identifier,
1303
+ index: i
1304
+ });
1305
+ }
1306
+ seenIdentifiers.add(identifier);
1307
+ }
1308
+ }
1309
+ }
1310
+ /**
1311
+ * Evaluate a logical constraint operator against its operands.
1312
+ * @param logicalConstraint The operator and operand list.
1313
+ * @param dataSources The operand lookup sources.
1314
+ * @returns True if the logical constraint is satisfied.
1315
+ * @internal
1316
+ */
1317
+ evaluateLogicalConstraint(logicalConstraint, dataSources) {
1318
+ const { operator, constraints } = logicalConstraint;
1319
+ if (constraints.length === 0) {
1320
+ return false;
1321
+ }
1322
+ switch (operator) {
1323
+ case OdrlLogicalConstraintType.And:
1324
+ return constraints.every(item => this.evaluateConstraint(item, dataSources));
1325
+ case OdrlLogicalConstraintType.AndSequence: {
1326
+ for (const item of constraints) {
1327
+ if (!this.evaluateConstraint(item, dataSources)) {
1328
+ return false;
1329
+ }
1330
+ }
1331
+ return true;
1332
+ }
1333
+ case OdrlLogicalConstraintType.Or:
1334
+ return constraints.some(item => this.evaluateConstraint(item, dataSources));
1335
+ case OdrlLogicalConstraintType.Xone: {
1336
+ let satisfied = 0;
1337
+ for (const item of constraints) {
1338
+ if (this.evaluateConstraint(item, dataSources)) {
1339
+ satisfied += 1;
1340
+ if (satisfied > 1) {
1341
+ return false;
1342
+ }
1343
+ }
1344
+ }
1345
+ return satisfied === 1;
1346
+ }
1347
+ default:
1348
+ return false;
1349
+ }
1350
+ }
1351
+ /**
1352
+ * Resolve a prefixed operand to its source object and JSONPath expression.
1353
+ * Prefix matching is dictionary-driven so additional operand namespaces can be
1354
+ * added in one place by extending the lookup sources map.
1355
+ * @param operandTypeOrValue The string operand value or typed operand namespace.
1356
+ * @param operandValue The JSONPath expression.
1357
+ * @param dataSources The available lookup sources.
1358
+ * @returns The resolved source and JSONPath, or undefined when not namespaced.
1359
+ * @throws GeneralError if a twin: prefixed operand doesn't resolve to any available datasource key.
1360
+ * @internal
1361
+ */
1362
+ tryResolveOperandLookup(operandTypeOrValue, operandValue, dataSources) {
1363
+ let sourceKey = this.normalizeTwinJsonPathOperandAlias(operandTypeOrValue);
1364
+ let value = operandValue;
1365
+ // Combined form: operandTypeOrValue is a full "<prefix>:<expression>" string
1366
+ // like "twin:jsonPath:$.foo" rather than the canonical separation of @type +
1367
+ // @value/expression. Detect by walking the registered datasource keys for a prefix
1368
+ // match; on hit, split into key + expression so resolveDataSourceByKey can evaluate.
1369
+ if (!dataSources[sourceKey]) {
1370
+ const matchingKey = Object.keys(dataSources).find(k => sourceKey.startsWith(`${k}:`));
1371
+ if (matchingKey) {
1372
+ value = sourceKey.slice(matchingKey.length + 1);
1373
+ sourceKey = matchingKey;
1374
+ }
1375
+ }
1376
+ if (!dataSources[sourceKey] || !Is.stringValue(value)) {
1377
+ return undefined;
1378
+ }
1379
+ const resolved = this.resolveDataSourceByKey(sourceKey, value, dataSources);
1380
+ return { source: resolved.source, jsonPath: resolved.target };
1381
+ }
1382
+ /**
1383
+ * Calculate an operand value.
1384
+ * @param operand The operand.
1385
+ * @param dataSources The available prefixed operand sources.
1386
+ * @returns The resolved operand value.
1387
+ * @internal
1388
+ */
1389
+ calculateOperandValue(operand, dataSources, constraint) {
1390
+ let jsonPath;
1391
+ let operandRoot;
1392
+ if (Is.stringValue(operand)) {
1393
+ const expression = this.extractJsonPathExpressionFromConstraint(constraint, operand);
1394
+ let resolvedOperand = operand;
1395
+ if (operand === DefaultPolicyArbiter._TWIN_JSONPATH &&
1396
+ Is.object(constraint)) {
1397
+ const dataSourceOverride = constraint[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE];
1398
+ if (Is.stringValue(dataSourceOverride)) {
1399
+ resolvedOperand = dataSourceOverride;
1400
+ }
1401
+ }
1402
+ const lookup = this.tryResolveOperandLookup(resolvedOperand, expression ?? operand, dataSources);
1403
+ if (lookup) {
1404
+ jsonPath = lookup.jsonPath;
1405
+ operandRoot = lookup.source;
1406
+ }
1407
+ }
1408
+ else if (Is.object(operand)) {
1409
+ const typedOperand = operand;
1410
+ // Is this an object { "@value": "18", "@type": "xsd:integer" } ?
1411
+ const value = this.extractJsonPathExpressionFromTypedOperand(typedOperand) ??
1412
+ typedOperand["@value"];
1413
+ const type = typedOperand["@type"];
1414
+ if (Is.stringValue(type)) {
1415
+ let resolvedType = type;
1416
+ if (this.isTwinJsonPathOperandType(type)) {
1417
+ const dataSourceOverride = typedOperand[DefaultPolicyArbiter._TWIN_JSONPATH_DATA_SOURCE];
1418
+ if (Is.stringValue(dataSourceOverride)) {
1419
+ resolvedType = dataSourceOverride;
1420
+ }
1421
+ }
1422
+ const lookup = this.tryResolveOperandLookup(resolvedType, value, dataSources);
1423
+ if (lookup) {
1424
+ jsonPath = lookup.jsonPath;
1425
+ operandRoot = lookup.source;
1426
+ }
1427
+ else {
1428
+ const xsdValue = this.coerceXsdType(value, type);
1429
+ if (!Is.undefined(xsdValue)) {
1430
+ return xsdValue;
1431
+ }
1432
+ }
1433
+ }
1434
+ }
1435
+ // We have a JSON Path to resolve
1436
+ if (Is.stringValue(jsonPath)) {
1437
+ const jsonPaths = JsonPathHelper.query(jsonPath, operandRoot);
1438
+ if (jsonPaths.length === 0) {
1439
+ // No matches
1440
+ return undefined;
1441
+ }
1442
+ else if (jsonPaths.length === 1) {
1443
+ // Single match - return the value directly
1444
+ return jsonPaths[0].value;
1445
+ }
1446
+ // Multiple matches - return array of values
1447
+ return jsonPaths.map(p => p.value);
1448
+ }
1449
+ // Not JSON Path or object value so return as is
1450
+ return operand;
1451
+ }
1452
+ /**
1453
+ * Determine if a target object uses the canonical twin:jsonPath object format.
1454
+ * @param target The target value.
1455
+ * @returns True if the target is a canonical twin:jsonPath object.
1456
+ * @internal
1457
+ */
1458
+ isTwinJsonPathTarget(target) {
1459
+ return Is.object(target) && this.isTwinJsonPathOperandType(OdrlPolicyHelper.getType(target));
1460
+ }
1461
+ /**
1462
+ * Determine if the operand type is a jsonPath namespace.
1463
+ * @param type The operand type.
1464
+ * @returns True if the type is twin:jsonPath.
1465
+ * @internal
1466
+ */
1467
+ isTwinJsonPathOperandType(type) {
1468
+ return type === DefaultPolicyArbiter._TWIN_JSONPATH;
1469
+ }
1470
+ /**
1471
+ * Normalize canonical jsonPath alias to the legacy jsonpath namespace key.
1472
+ * @param operandTypeOrValue The raw operand type or value.
1473
+ * @returns The normalized operand type/value.
1474
+ * @internal
1475
+ */
1476
+ normalizeTwinJsonPathOperandAlias(operandTypeOrValue) {
1477
+ if (operandTypeOrValue === DefaultPolicyArbiter._TWIN_JSONPATH ||
1478
+ operandTypeOrValue.startsWith(`${DefaultPolicyArbiter._TWIN_JSONPATH}:`)) {
1479
+ const suffix = operandTypeOrValue.slice(DefaultPolicyArbiter._TWIN_JSONPATH.length);
1480
+ return `${DefaultPolicyArbiter._DATA_SOURCE_KEY}${suffix}`;
1481
+ }
1482
+ return operandTypeOrValue;
1483
+ }
1484
+ /**
1485
+ * Extract canonical jsonPath expression from typed operand object.
1486
+ * @param operand The typed operand.
1487
+ * @returns The jsonPath expression if present.
1488
+ * @internal
1489
+ */
1490
+ extractJsonPathExpressionFromTypedOperand(operand) {
1491
+ if (!this.isTwinJsonPathOperandType(operand["@type"])) {
1492
+ return undefined;
1493
+ }
1494
+ const expression = operand[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
1495
+ if (Is.stringValue(expression)) {
1496
+ return expression;
1497
+ }
1498
+ const type = operand["@type"];
1499
+ if (type === DefaultPolicyArbiter._TWIN_JSONPATH) {
1500
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
1501
+ operand: "rightOperand"
1502
+ });
1503
+ }
1504
+ }
1505
+ /**
1506
+ * Extract canonical jsonPath expression from constraint for leftOperand aliases.
1507
+ * @param constraint The constraint containing the left operand.
1508
+ * @param leftOperand The left operand.
1509
+ * @returns The expression if canonical form is used.
1510
+ * @internal
1511
+ */
1512
+ extractJsonPathExpressionFromConstraint(constraint, leftOperand) {
1513
+ if (!Is.object(constraint)) {
1514
+ return undefined;
1515
+ }
1516
+ if (constraint.leftOperand !== leftOperand) {
1517
+ return undefined;
1518
+ }
1519
+ if (leftOperand.startsWith(`${DefaultPolicyArbiter._TWIN_JSONPATH}:`)) {
1520
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
1521
+ operand: "leftOperand"
1522
+ });
1523
+ }
1524
+ if (leftOperand !== DefaultPolicyArbiter._TWIN_JSONPATH) {
1525
+ return undefined;
1526
+ }
1527
+ const expression = constraint[DefaultPolicyArbiter._TWIN_JSONPATH_EXPRESSION];
1528
+ if (Is.stringValue(expression)) {
1529
+ return expression;
1530
+ }
1531
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
1532
+ operand: "leftOperand"
1533
+ });
1534
+ }
1535
+ /**
1536
+ * Evaluate an ODRL operator against resolved operands.
1537
+ * @param operator The operator.
1538
+ * @param left The resolved left operand.
1539
+ * @param right The resolved right operand.
1540
+ * @returns True if the comparison is satisfied.
1541
+ * @internal
1542
+ */
1543
+ evaluateOperator(operator, left, right) {
1544
+ // Handle array/collection left values (e.g. JSONPath returning multiple matches).
1545
+ const leftValues = ArrayHelper.fromObjectOrArray(left ?? []);
1546
+ switch (operator) {
1547
+ case OdrlOperatorType.Eq:
1548
+ return leftValues.some(v => ObjectHelper.equal(v, right, false));
1549
+ case OdrlOperatorType.Neq:
1550
+ return leftValues.every(v => !ObjectHelper.equal(v, right, false));
1551
+ case OdrlOperatorType.Gt:
1552
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a > b));
1553
+ case OdrlOperatorType.Gteq:
1554
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a >= b));
1555
+ case OdrlOperatorType.Lt:
1556
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a < b));
1557
+ case OdrlOperatorType.Lteq:
1558
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a <= b));
1559
+ case OdrlOperatorType.IsAnyOf: {
1560
+ return leftValues.some(v => {
1561
+ const stringValue = typeof v === "string" || typeof v === "number" || typeof v === "boolean"
1562
+ ? String(v)
1563
+ : JSON.stringify(v);
1564
+ return (ArrayHelper.fromObjectOrArray(right) ?? []).includes(stringValue);
1565
+ });
1566
+ }
1567
+ case OdrlOperatorType.IsAllOf: {
1568
+ return ObjectHelper.equal(leftValues, ArrayHelper.fromObjectOrArray(right) ?? [], false);
1569
+ }
1570
+ case OdrlOperatorType.IsNoneOf: {
1571
+ return leftValues.every(v => !(ArrayHelper.fromObjectOrArray(right) ?? []).includes(v));
1572
+ }
1573
+ case OdrlOperatorType.LocTimeEq:
1574
+ return leftValues.some(v => ObjectHelper.equal(v, right, false));
1575
+ case OdrlOperatorType.LocTimeGteq:
1576
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a >= b));
1577
+ case OdrlOperatorType.IsA:
1578
+ case OdrlOperatorType.HasPart:
1579
+ case OdrlOperatorType.IsPartOf:
1580
+ // For now, treat these as simple equality/ordering semantics where meaningful.
1581
+ // Profiles can introduce richer semantics via additional arbiters.
1582
+ return leftValues.some(v => ObjectHelper.equal(v, right, false));
1583
+ default:
1584
+ return false;
1585
+ }
1586
+ }
1587
+ /**
1588
+ * Compare values with numeric/date/string coercion.
1589
+ * @param left The left value.
1590
+ * @param right The right value.
1591
+ * @param compare Comparison operator.
1592
+ * @returns True if ordered comparison passes.
1593
+ * @internal
1594
+ */
1595
+ compareOrdered(left, right, compare) {
1596
+ const leftNum = Coerce.number(left);
1597
+ const rightNum = Coerce.number(right);
1598
+ if (!Is.undefined(leftNum) && !Is.undefined(rightNum)) {
1599
+ return compare(leftNum, rightNum);
1600
+ }
1601
+ const leftDate = Coerce.dateTime(left);
1602
+ const rightDate = Coerce.dateTime(right);
1603
+ if (!Is.undefined(leftDate) && !Is.undefined(rightDate)) {
1604
+ return compare(leftDate.getTime(), rightDate.getTime());
1605
+ }
1606
+ // Only use string ordering when both operands are actual strings.
1607
+ // Avoid coercing other types into strings, as that can cause
1608
+ // unintended comparisons like 18 >= "$.minAge" evaluating to true.
1609
+ if (Is.string(left) && Is.string(right)) {
1610
+ return compare(left.localeCompare(right), 0);
1611
+ }
1612
+ return false;
1613
+ }
1614
+ /**
1615
+ * Coerce a value to a specific XSD type.
1616
+ * @param value The value to coerce.
1617
+ * @param type The XSD type.
1618
+ * @returns The coerced value, or undefined when coercion is not possible.
1619
+ * @internal
1620
+ */
1621
+ coerceXsdType(value, type) {
1622
+ if ([
1623
+ "xsd:string",
1624
+ "xsd:normalizedString",
1625
+ "xsd:token",
1626
+ "xsd:anyURI",
1627
+ "xsd:QName",
1628
+ "xsd:NOTATION"
1629
+ ].includes(type)) {
1630
+ // Handle standard xsd types
1631
+ return Coerce.string(value);
1632
+ }
1633
+ else if ([
1634
+ "xsd:integer",
1635
+ "xsd:decimal",
1636
+ "xsd:float",
1637
+ "xsd:double",
1638
+ "xsd:long",
1639
+ "xsd:int",
1640
+ "xsd:short",
1641
+ "xsd:byte"
1642
+ ].includes(type)) {
1643
+ // Handle standard xsd types
1644
+ return Coerce.number(value);
1645
+ }
1646
+ else if (type === "xsd:boolean") {
1647
+ // Handle standard xsd types
1648
+ return Coerce.boolean(value);
1649
+ }
1650
+ else if (["xsd:date", "xsd:dateTime", "xsd:time"].includes(type)) {
1651
+ // Handle standard xsd types
1652
+ return Coerce.dateTime(value);
1653
+ }
1654
+ return undefined;
1655
+ }
1656
+ }
1657
+ //# sourceMappingURL=defaultPolicyArbiter.js.map