@3sixty/rights-management-plugins 0.0.0-stage → 0.20.0-next.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 (134) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +32 -2
  3. package/dist/es/index.js +34 -0
  4. package/dist/es/index.js.map +1 -0
  5. package/dist/es/models/IAutomationPolicyExecutionActionConfig.js +4 -0
  6. package/dist/es/models/IAutomationPolicyExecutionActionConfig.js.map +1 -0
  7. package/dist/es/models/IAutomationPolicyExecutionActionConstructorOptions.js +2 -0
  8. package/dist/es/models/IAutomationPolicyExecutionActionConstructorOptions.js.map +1 -0
  9. package/dist/es/models/IDefaultPolicyArbiterConfig.js +4 -0
  10. package/dist/es/models/IDefaultPolicyArbiterConfig.js.map +1 -0
  11. package/dist/es/models/IDefaultPolicyArbiterConstructorOptions.js +4 -0
  12. package/dist/es/models/IDefaultPolicyArbiterConstructorOptions.js.map +1 -0
  13. package/dist/es/models/IDefaultPolicyEnforcementProcessorConfig.js +4 -0
  14. package/dist/es/models/IDefaultPolicyEnforcementProcessorConfig.js.map +1 -0
  15. package/dist/es/models/IDefaultPolicyEnforcementProcessorConstructorOptions.js +2 -0
  16. package/dist/es/models/IDefaultPolicyEnforcementProcessorConstructorOptions.js.map +1 -0
  17. package/dist/es/models/IIdentityPolicyInformationSourceConstructorOptions.js +4 -0
  18. package/dist/es/models/IIdentityPolicyInformationSourceConstructorOptions.js.map +1 -0
  19. package/dist/es/models/IIdentityProfilePolicyInformationSourceConstructorOptions.js +4 -0
  20. package/dist/es/models/IIdentityProfilePolicyInformationSourceConstructorOptions.js.map +1 -0
  21. package/dist/es/models/ILoggingPolicyExecutionActionConfig.js +2 -0
  22. package/dist/es/models/ILoggingPolicyExecutionActionConfig.js.map +1 -0
  23. package/dist/es/models/ILoggingPolicyExecutionActionConstructorOptions.js +2 -0
  24. package/dist/es/models/ILoggingPolicyExecutionActionConstructorOptions.js.map +1 -0
  25. package/dist/es/models/IPassThroughPolicyArbiterConstructorOptions.js +4 -0
  26. package/dist/es/models/IPassThroughPolicyArbiterConstructorOptions.js.map +1 -0
  27. package/dist/es/models/IPassThroughPolicyEnforcementProcessorConstructorOptions.js +4 -0
  28. package/dist/es/models/IPassThroughPolicyEnforcementProcessorConstructorOptions.js.map +1 -0
  29. package/dist/es/models/IPassThroughPolicyNegotiatorConfig.js +4 -0
  30. package/dist/es/models/IPassThroughPolicyNegotiatorConfig.js.map +1 -0
  31. package/dist/es/models/IPassThroughPolicyNegotiatorConstructorOptions.js +2 -0
  32. package/dist/es/models/IPassThroughPolicyNegotiatorConstructorOptions.js.map +1 -0
  33. package/dist/es/models/IPassThroughPolicyObligationEnforcerConstructorOptions.js +4 -0
  34. package/dist/es/models/IPassThroughPolicyObligationEnforcerConstructorOptions.js.map +1 -0
  35. package/dist/es/models/IPassThroughPolicyRequesterConstructorOptions.js +4 -0
  36. package/dist/es/models/IPassThroughPolicyRequesterConstructorOptions.js.map +1 -0
  37. package/dist/es/models/IStaticPolicyInformationSource.js +2 -0
  38. package/dist/es/models/IStaticPolicyInformationSource.js.map +1 -0
  39. package/dist/es/models/IStaticPolicyInformationSourceConfig.js +2 -0
  40. package/dist/es/models/IStaticPolicyInformationSourceConfig.js.map +1 -0
  41. package/dist/es/models/IStaticPolicyInformationSourceConstructorOptions.js +2 -0
  42. package/dist/es/models/IStaticPolicyInformationSourceConstructorOptions.js.map +1 -0
  43. package/dist/es/policyArbiters/defaultPolicyArbiter.js +1880 -0
  44. package/dist/es/policyArbiters/defaultPolicyArbiter.js.map +1 -0
  45. package/dist/es/policyArbiters/passThroughPolicyArbiter.js +59 -0
  46. package/dist/es/policyArbiters/passThroughPolicyArbiter.js.map +1 -0
  47. package/dist/es/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.js +198 -0
  48. package/dist/es/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.js.map +1 -0
  49. package/dist/es/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.js +54 -0
  50. package/dist/es/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.js.map +1 -0
  51. package/dist/es/policyExecutionActions/automationPolicyExecutionAction.js +77 -0
  52. package/dist/es/policyExecutionActions/automationPolicyExecutionAction.js.map +1 -0
  53. package/dist/es/policyExecutionActions/loggingPolicyExecutionAction.js +121 -0
  54. package/dist/es/policyExecutionActions/loggingPolicyExecutionAction.js.map +1 -0
  55. package/dist/es/policyInformationSources/identityPolicyInformationSource.js +91 -0
  56. package/dist/es/policyInformationSources/identityPolicyInformationSource.js.map +1 -0
  57. package/dist/es/policyInformationSources/identityProfilePolicyInformationSource.js +112 -0
  58. package/dist/es/policyInformationSources/identityProfilePolicyInformationSource.js.map +1 -0
  59. package/dist/es/policyInformationSources/staticPolicyInformationSource.js +96 -0
  60. package/dist/es/policyInformationSources/staticPolicyInformationSource.js.map +1 -0
  61. package/dist/es/policyNegotiators/passThroughPolicyNegotiator.js +95 -0
  62. package/dist/es/policyNegotiators/passThroughPolicyNegotiator.js.map +1 -0
  63. package/dist/es/policyObligationEnforcers/passThroughPolicyObligationEnforcer.js +56 -0
  64. package/dist/es/policyObligationEnforcers/passThroughPolicyObligationEnforcer.js.map +1 -0
  65. package/dist/es/policyRequesters/passThroughPolicyRequester.js +103 -0
  66. package/dist/es/policyRequesters/passThroughPolicyRequester.js.map +1 -0
  67. package/dist/types/index.d.ts +31 -0
  68. package/dist/types/models/IAutomationPolicyExecutionActionConfig.d.ts +9 -0
  69. package/dist/types/models/IAutomationPolicyExecutionActionConstructorOptions.d.ts +15 -0
  70. package/dist/types/models/IDefaultPolicyArbiterConfig.d.ts +10 -0
  71. package/dist/types/models/IDefaultPolicyArbiterConstructorOptions.d.ts +19 -0
  72. package/dist/types/models/IDefaultPolicyEnforcementProcessorConfig.d.ts +11 -0
  73. package/dist/types/models/IDefaultPolicyEnforcementProcessorConstructorOptions.d.ts +14 -0
  74. package/dist/types/models/IIdentityPolicyInformationSourceConstructorOptions.d.ts +14 -0
  75. package/dist/types/models/IIdentityProfilePolicyInformationSourceConstructorOptions.d.ts +14 -0
  76. package/dist/types/models/ILoggingPolicyExecutionActionConfig.d.ts +25 -0
  77. package/dist/types/models/ILoggingPolicyExecutionActionConstructorOptions.d.ts +14 -0
  78. package/dist/types/models/IPassThroughPolicyArbiterConstructorOptions.d.ts +9 -0
  79. package/dist/types/models/IPassThroughPolicyEnforcementProcessorConstructorOptions.d.ts +9 -0
  80. package/dist/types/models/IPassThroughPolicyNegotiatorConfig.d.ts +9 -0
  81. package/dist/types/models/IPassThroughPolicyNegotiatorConstructorOptions.d.ts +14 -0
  82. package/dist/types/models/IPassThroughPolicyObligationEnforcerConstructorOptions.d.ts +9 -0
  83. package/dist/types/models/IPassThroughPolicyRequesterConstructorOptions.d.ts +9 -0
  84. package/dist/types/models/IStaticPolicyInformationSource.d.ts +23 -0
  85. package/dist/types/models/IStaticPolicyInformationSourceConfig.d.ts +10 -0
  86. package/dist/types/models/IStaticPolicyInformationSourceConstructorOptions.d.ts +14 -0
  87. package/dist/types/policyArbiters/defaultPolicyArbiter.d.ts +38 -0
  88. package/dist/types/policyArbiters/passThroughPolicyArbiter.d.ts +32 -0
  89. package/dist/types/policyEnforcementProcessor/defaultPolicyEnforcementProcessor.d.ts +36 -0
  90. package/dist/types/policyEnforcementProcessor/passThroughPolicyEnforcementProcessor.d.ts +32 -0
  91. package/dist/types/policyExecutionActions/automationPolicyExecutionAction.d.ts +45 -0
  92. package/dist/types/policyExecutionActions/loggingPolicyExecutionAction.d.ts +37 -0
  93. package/dist/types/policyInformationSources/identityPolicyInformationSource.d.ts +31 -0
  94. package/dist/types/policyInformationSources/identityProfilePolicyInformationSource.d.ts +31 -0
  95. package/dist/types/policyInformationSources/staticPolicyInformationSource.d.ts +37 -0
  96. package/dist/types/policyNegotiators/passThroughPolicyNegotiator.d.ts +48 -0
  97. package/dist/types/policyObligationEnforcers/passThroughPolicyObligationEnforcer.d.ts +32 -0
  98. package/dist/types/policyRequesters/passThroughPolicyRequester.d.ts +48 -0
  99. package/docs/changelog.md +0 -0
  100. package/docs/examples.md +295 -0
  101. package/docs/reference/classes/AutomationPolicyExecutionAction.md +143 -0
  102. package/docs/reference/classes/DefaultPolicyArbiter.md +113 -0
  103. package/docs/reference/classes/DefaultPolicyEnforcementProcessor.md +115 -0
  104. package/docs/reference/classes/IdentityPolicyInformationSource.md +103 -0
  105. package/docs/reference/classes/IdentityProfilePolicyInformationSource.md +103 -0
  106. package/docs/reference/classes/LoggingPolicyExecutionAction.md +127 -0
  107. package/docs/reference/classes/PassThroughPolicyArbiter.md +103 -0
  108. package/docs/reference/classes/PassThroughPolicyEnforcementProcessor.md +107 -0
  109. package/docs/reference/classes/PassThroughPolicyNegotiator.md +149 -0
  110. package/docs/reference/classes/PassThroughPolicyObligationEnforcer.md +109 -0
  111. package/docs/reference/classes/PassThroughPolicyRequester.md +169 -0
  112. package/docs/reference/classes/StaticPolicyInformationSource.md +123 -0
  113. package/docs/reference/index.md +38 -0
  114. package/docs/reference/interfaces/IAutomationPolicyExecutionActionConfig.md +11 -0
  115. package/docs/reference/interfaces/IAutomationPolicyExecutionActionConstructorOptions.md +25 -0
  116. package/docs/reference/interfaces/IDefaultPolicyArbiterConfig.md +17 -0
  117. package/docs/reference/interfaces/IDefaultPolicyArbiterConstructorOptions.md +33 -0
  118. package/docs/reference/interfaces/IDefaultPolicyEnforcementProcessorConfig.md +13 -0
  119. package/docs/reference/interfaces/IDefaultPolicyEnforcementProcessorConstructorOptions.md +19 -0
  120. package/docs/reference/interfaces/IIdentityPolicyInformationSourceConstructorOptions.md +25 -0
  121. package/docs/reference/interfaces/IIdentityProfilePolicyInformationSourceConstructorOptions.md +25 -0
  122. package/docs/reference/interfaces/ILoggingPolicyExecutionActionConfig.md +53 -0
  123. package/docs/reference/interfaces/ILoggingPolicyExecutionActionConstructorOptions.md +19 -0
  124. package/docs/reference/interfaces/IPassThroughPolicyArbiterConstructorOptions.md +11 -0
  125. package/docs/reference/interfaces/IPassThroughPolicyEnforcementProcessorConstructorOptions.md +11 -0
  126. package/docs/reference/interfaces/IPassThroughPolicyNegotiatorConfig.md +11 -0
  127. package/docs/reference/interfaces/IPassThroughPolicyNegotiatorConstructorOptions.md +19 -0
  128. package/docs/reference/interfaces/IPassThroughPolicyObligationEnforcerConstructorOptions.md +11 -0
  129. package/docs/reference/interfaces/IPassThroughPolicyRequesterConstructorOptions.md +11 -0
  130. package/docs/reference/interfaces/IStaticPolicyInformationSource.md +43 -0
  131. package/docs/reference/interfaces/IStaticPolicyInformationSourceConfig.md +11 -0
  132. package/docs/reference/interfaces/IStaticPolicyInformationSourceConstructorOptions.md +19 -0
  133. package/locales/en.json +79 -0
  134. package/package.json +60 -5
@@ -0,0 +1,1880 @@
1
+ // Copyright 2025 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { ArrayHelper, Coerce, ComponentFactory, Duration, GeneralError, Guards, Is, ObjectHelper, StringHelper } from "@3sixty/core";
4
+ import { JsonPathHelper } from "@3sixty/data-json-path";
5
+ import { OdrlPolicyHelper, OdrlProfiles, OdrlTwinVocabulary, PolicyDecision, PolicyObligationEnforcerFactory } from "@3sixty/rights-management-models";
6
+ import { OdrlConflictStrategyType, OdrlLeftOperandType, OdrlLogicalConstraintType, OdrlOperatorType, OdrlTypes } from "@3sixty/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
+ * The logging component.
28
+ * @internal
29
+ */
30
+ _logging;
31
+ /**
32
+ * The policy administration point component.
33
+ * @internal
34
+ */
35
+ _policyAdministrationPoint;
36
+ /**
37
+ * The maximum depth to traverse when resolving inherited policies.
38
+ * @internal
39
+ */
40
+ _maxInheritanceDepth;
41
+ /**
42
+ * Create a new instance of DefaultPolicyArbiter.
43
+ * @param options The options for the default policy arbiter.
44
+ */
45
+ constructor(options) {
46
+ this._logging = ComponentFactory.getIfExists(options?.loggingComponentType);
47
+ this._policyAdministrationPoint = ComponentFactory.get(options?.policyAdministrationPointComponentType ?? "policy-administration-point");
48
+ this._maxInheritanceDepth =
49
+ Coerce.integer(options?.config?.maxInheritanceDepth) ??
50
+ DefaultPolicyArbiter._DEFAULT_MAX_INHERITANCE_DEPTH;
51
+ }
52
+ /**
53
+ * Normalises a profile IRI for comparison against the supported-profiles allowlist.
54
+ * Per RFC 3986: scheme and host are case-insensitive (the URL constructor folds them to
55
+ * lowercase automatically); a trailing slash on the terminal path segment is treated as
56
+ * equivalent to its absence; repeated slashes in the path are collapsed to a single slash.
57
+ * Non-URL strings are returned unchanged.
58
+ * @param iri The IRI to normalise.
59
+ * @returns The normalised IRI.
60
+ * @internal
61
+ */
62
+ static normalizeProfileIri(iri) {
63
+ try {
64
+ const url = new URL(iri);
65
+ const pathname = url.pathname.replace(/\/+/g, "/");
66
+ return `${url.protocol}//${url.host}${StringHelper.trimTrailingSlashes(pathname)}`;
67
+ }
68
+ catch {
69
+ return iri;
70
+ }
71
+ }
72
+ /**
73
+ * Returns the class name of the component.
74
+ * @returns The class name of the component.
75
+ */
76
+ className() {
77
+ return DefaultPolicyArbiter.CLASS_NAME;
78
+ }
79
+ /**
80
+ * Makes decisions regarding policy access to data.
81
+ * @param agreement The agreement to evaluate.
82
+ * @param information Information provided by the requester to determine if a policy can be created.
83
+ * @param data The data to make a decision on.
84
+ * @param action Optional action to make a decision on, if not provided, the arbiter will evaluate all actions in the agreement.
85
+ * @returns The decisions about access to the data.
86
+ */
87
+ async decide(agreement, information, data, action) {
88
+ Guards.object(DefaultPolicyArbiter.CLASS_NAME, "agreement", agreement);
89
+ // ODRL policy profiles extend the vocabulary with additional semantics (e.g. custom
90
+ // operators, left operands). Without profile-aware evaluation logic the arbiter
91
+ // cannot guarantee correctness, so any policy that declares an unknown profile is
92
+ // rejected. The TWIN platform profile is explicitly supported.
93
+ //
94
+ // Empty-string profile values (e.g. from over-eager schema defaults or serialization
95
+ // round-trips) are treated as "no profile declared" and filtered out before comparison.
96
+ //
97
+ // Profile IRIs are normalized before lookup: scheme and host are case-folded to
98
+ // lowercase and a trailing slash on the last path segment is stripped. This accepts
99
+ // common IRI variants (HTTPS://, uppercase host, trailing slash) instead of silently
100
+ // rejecting valid policies authored by IRI-aware tooling.
101
+ //
102
+ // `every` (conjunction) is intentional: a policy declaring ["TWIN", "unknown"] is
103
+ // rejected - the arbiter refuses to evaluate rules from a profile whose semantics it
104
+ // does not understand, even if other declared profiles are known.
105
+ const declaredProfiles = ArrayHelper.fromObjectOrArray(agreement.profile ?? [])
106
+ .filter(p => Is.stringValue(p))
107
+ .map(p => DefaultPolicyArbiter.normalizeProfileIri(p));
108
+ if (Is.arrayValue(declaredProfiles) &&
109
+ !declaredProfiles.every(p => DefaultPolicyArbiter.SUPPORTED_PROFILES.has(p))) {
110
+ const unsupportedProfile = declaredProfiles.find(p => !DefaultPolicyArbiter.SUPPORTED_PROFILES.has(p));
111
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "policyProfileNotSupported", {
112
+ policyId: OdrlPolicyHelper.getUid(agreement) ?? "",
113
+ unsupportedProfile
114
+ });
115
+ }
116
+ await this._logging?.log({
117
+ level: "info",
118
+ source: DefaultPolicyArbiter.CLASS_NAME,
119
+ ts: Date.now(),
120
+ message: "decidingPolicy",
121
+ data: {
122
+ policyId: OdrlPolicyHelper.getUid(agreement) ?? ""
123
+ }
124
+ });
125
+ // Resolve and merge inherited policies.
126
+ const mergedPolicy = await this.mergeInheritedPolicies(agreement);
127
+ const expandedPolicy = this.expandCompactPolicyRules(mergedPolicy);
128
+ const dataSources = {
129
+ [OdrlTwinVocabulary.DataSourceKey]: data,
130
+ [OdrlTwinVocabulary.InformationSourceKey]: information
131
+ };
132
+ // Extract agreement parties once for use in rule evaluation
133
+ const agreementAssigner = OdrlPolicyHelper.getPartyIds(agreement.assigner);
134
+ const agreementAssignee = OdrlPolicyHelper.getPartyIds(agreement.assignee);
135
+ const policyTargetId = this.tryGetPolicyAssetTargetId(expandedPolicy);
136
+ const obligationsFulfilled = await this.evaluatePolicyObligations(agreementAssigner, agreementAssignee, expandedPolicy, dataSources);
137
+ // ODRL-style rule evaluation grouped by decision target:
138
+ // - Permission rules authorize if ANY applicable permission on the same target matches.
139
+ // - Default to denied when no permission applies on a target (closed-world for access control).
140
+ // - Conflict strategy controls how applicable permissions/prohibitions are resolved per target.
141
+ const targetStates = Object.create(null);
142
+ const permissions = ArrayHelper.fromObjectOrArray(expandedPolicy.permission ?? []);
143
+ for (const permission of permissions) {
144
+ const decisionTargets = this.resolveRuleDecisionTargets(permission, dataSources, policyTargetId);
145
+ for (const decisionTarget of decisionTargets) {
146
+ const state = this.getOrCreateTargetState(targetStates, decisionTarget.target);
147
+ if (await this.evaluatePermission(agreementAssigner, agreementAssignee, expandedPolicy, permission, decisionTarget.refinements, dataSources, action, decisionTarget.target)) {
148
+ state.permissionApplies = true;
149
+ }
150
+ }
151
+ }
152
+ const prohibitions = ArrayHelper.fromObjectOrArray(expandedPolicy.prohibition ?? []);
153
+ for (const prohibition of prohibitions) {
154
+ const decisionTargets = this.resolveRuleDecisionTargets(prohibition, dataSources, policyTargetId);
155
+ for (const decisionTarget of decisionTargets) {
156
+ if (await this.evaluateProhibition(expandedPolicy, agreementAssigner, agreementAssignee, prohibition, decisionTarget.refinements, dataSources, action, decisionTarget.target)) {
157
+ const state = this.getOrCreateTargetState(targetStates, decisionTarget.target);
158
+ state.prohibitionApplies = true;
159
+ }
160
+ }
161
+ }
162
+ const conflictStrategy = expandedPolicy.conflict ?? OdrlConflictStrategyType.Invalid;
163
+ if (Object.keys(targetStates).length === 0) {
164
+ // Closed-world fallback when the policy has no rules at all.
165
+ return [{ decision: PolicyDecision.Denied, target: "$" }];
166
+ }
167
+ const decisions = [];
168
+ for (const [target, state] of Object.entries(targetStates)) {
169
+ let decision;
170
+ if (state.permissionApplies && state.prohibitionApplies) {
171
+ switch (conflictStrategy) {
172
+ case OdrlConflictStrategyType.Perm:
173
+ decision = PolicyDecision.Granted;
174
+ break;
175
+ case OdrlConflictStrategyType.Prohibit:
176
+ case OdrlConflictStrategyType.Invalid:
177
+ default:
178
+ decision = PolicyDecision.Denied;
179
+ break;
180
+ }
181
+ }
182
+ else {
183
+ decision = state.permissionApplies ? PolicyDecision.Granted : PolicyDecision.Denied;
184
+ }
185
+ if (!obligationsFulfilled) {
186
+ decision = PolicyDecision.Denied;
187
+ }
188
+ decisions.push({
189
+ decision,
190
+ target
191
+ });
192
+ }
193
+ return decisions;
194
+ }
195
+ /**
196
+ * Expand compact/compound policy rule forms into atomic rules.
197
+ * ODRL 2.7 allows compact forms where rule properties can be arrays.
198
+ * Evaluation in this arbiter is performed on expanded atomic rules.
199
+ * @param policy The policy to expand.
200
+ * @returns A policy with expanded rule arrays.
201
+ * @internal
202
+ */
203
+ expandCompactPolicyRules(policy) {
204
+ const expandedPermissions = this.expandRules(ArrayHelper.fromObjectOrArray(policy.permission ?? []));
205
+ const expandedProhibitions = this.expandRules(ArrayHelper.fromObjectOrArray(policy.prohibition ?? []));
206
+ const expandedObligations = this.expandRules(ArrayHelper.fromObjectOrArray(policy.obligation ?? []));
207
+ return {
208
+ ...policy,
209
+ permission: expandedPermissions.length > 0 ? expandedPermissions : undefined,
210
+ prohibition: expandedProhibitions.length > 0 ? expandedProhibitions : undefined,
211
+ obligation: expandedObligations.length > 0 ? expandedObligations : undefined
212
+ };
213
+ }
214
+ /**
215
+ * Expand a rule list into atomic rules.
216
+ * @param rules The rules to expand.
217
+ * @returns Expanded atomic rules.
218
+ * @internal
219
+ */
220
+ expandRules(rules) {
221
+ const expanded = [];
222
+ for (const rule of rules) {
223
+ expanded.push(...this.expandRule(rule));
224
+ }
225
+ return expanded;
226
+ }
227
+ /**
228
+ * Expand a single compact/compound rule into atomic rules.
229
+ * @param rule The rule to expand.
230
+ * @returns Expanded atomic rules.
231
+ * @internal
232
+ */
233
+ expandRule(rule) {
234
+ const targets = this.normalizeRuleField(rule.target);
235
+ const actions = this.normalizeRuleField(rule.action);
236
+ const assigners = this.normalizeRuleField(rule.assigner);
237
+ const assignees = this.normalizeRuleField(rule.assignee);
238
+ const expanded = [];
239
+ for (const target of targets) {
240
+ for (const action of actions) {
241
+ for (const assigner of assigners) {
242
+ for (const assignee of assignees) {
243
+ const atomicRule = { ...rule };
244
+ atomicRule.target = target;
245
+ atomicRule.action = action;
246
+ atomicRule.assigner = assigner;
247
+ atomicRule.assignee = assignee;
248
+ expanded.push(atomicRule);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ return expanded;
254
+ }
255
+ /**
256
+ * Normalize a potentially compact rule field into an array for expansion.
257
+ * @param value The field value.
258
+ * @returns Normalized values (or a single undefined when not provided).
259
+ * @internal
260
+ */
261
+ normalizeRuleField(value) {
262
+ if (Is.undefined(value)) {
263
+ return [undefined];
264
+ }
265
+ const values = ArrayHelper.fromObjectOrArray(value);
266
+ return Is.arrayValue(values) ? values : [undefined];
267
+ }
268
+ /**
269
+ * Get an existing target state or create an initial state if it doesn't exist.
270
+ * @param targetStates The dictionary of target states.
271
+ * @param target The target key.
272
+ * @returns The target state.
273
+ * @internal
274
+ */
275
+ getOrCreateTargetState(targetStates, target) {
276
+ let state = targetStates[target];
277
+ if (Is.undefined(state)) {
278
+ state = {
279
+ permissionApplies: false,
280
+ prohibitionApplies: false
281
+ };
282
+ targetStates[target] = state;
283
+ }
284
+ return state;
285
+ }
286
+ /**
287
+ * Evaluate whether a prohibition applies.
288
+ * @param policy The policy containing the prohibition.
289
+ * @param agreementAssigner The assigner ID from the agreement.
290
+ * @param agreementAssignee The assignee ID from the agreement.
291
+ * @param prohibition The prohibition to evaluate.
292
+ * @param targetRefinements Additional constraints from target refinement.
293
+ * @param dataSources The operand lookup sources.
294
+ * @param action Optional action to check against the prohibition's applicable actions.
295
+ * @param decisionTarget Optional target identifier for scoped decisions.
296
+ * @returns True if the prohibition applies.
297
+ * @internal
298
+ */
299
+ async evaluateProhibition(policy, agreementAssigner, agreementAssignee, prohibition, targetRefinements, dataSources, action, decisionTarget) {
300
+ if (!this.isRuleApplicableToParties(prohibition, agreementAssigner, agreementAssignee, dataSources)) {
301
+ return false;
302
+ }
303
+ // Check if the prohibition's action(s) match the requested action
304
+ if (!this.isActionApplicable(prohibition.action, action, dataSources)) {
305
+ return false;
306
+ }
307
+ // ODRL semantics: a rule without constraints is unconditional.
308
+ const constraints = [
309
+ ...ArrayHelper.fromObjectOrArray(prohibition.constraint ?? []),
310
+ ...targetRefinements
311
+ ];
312
+ if (constraints.length > 0 &&
313
+ !constraints.every(c => this.evaluateConstraint(c, dataSources))) {
314
+ return false;
315
+ }
316
+ const prohibitionTargetLookup = this.tryResolveTargetDataSource(this.buildRuleDataContextTargetId(this.getRuleDataContextTargetId(prohibition.target), decisionTarget), dataSources, true, this.tryGetPolicyAssetTargetId(policy));
317
+ const ruleDataContext = prohibitionTargetLookup.value;
318
+ const remedies = ArrayHelper.fromObjectOrArray(prohibition.remedy ?? []);
319
+ if (remedies.length === 0) {
320
+ return true;
321
+ }
322
+ for (const remedy of remedies) {
323
+ if (!(await this.enforceDuty(policy, remedy, dataSources, ruleDataContext))) {
324
+ return true;
325
+ }
326
+ }
327
+ // Remedies satisfied: prohibition is treated as no longer infringed.
328
+ return false;
329
+ }
330
+ /**
331
+ * Evaluate all policy-level obligations.
332
+ * @param agreementAssigner The assigner ID from the agreement.
333
+ * @param agreementAssignee The assignee ID from the agreement.
334
+ * @param policy The policy containing obligations.
335
+ * @param dataSources The operand lookup sources.
336
+ * @returns True if all applicable obligations are fulfilled.
337
+ * @internal
338
+ */
339
+ async evaluatePolicyObligations(agreementAssigner, agreementAssignee, policy, dataSources) {
340
+ const obligations = ArrayHelper.fromObjectOrArray(policy.obligation ?? []);
341
+ for (const obligation of obligations) {
342
+ if (!(await this.evaluateObligation(agreementAssigner, agreementAssignee, policy, obligation, dataSources))) {
343
+ return false;
344
+ }
345
+ }
346
+ return true;
347
+ }
348
+ /**
349
+ * Evaluate whether a policy-level obligation is fulfilled.
350
+ * @param agreementAssigner The assigner ID from the agreement.
351
+ * @param agreementAssignee The assignee ID from the agreement.
352
+ * @param policy The policy containing the obligation.
353
+ * @param obligation The obligation to evaluate.
354
+ * @param dataSources The operand lookup sources.
355
+ * @returns True if the obligation is not applicable or is fulfilled.
356
+ * @internal
357
+ */
358
+ async evaluateObligation(agreementAssigner, agreementAssignee, policy, obligation, dataSources) {
359
+ if (!this.isRuleApplicableToParties(obligation, agreementAssigner, agreementAssignee, dataSources)) {
360
+ return true;
361
+ }
362
+ const policyTargetId = this.tryGetPolicyAssetTargetId(policy);
363
+ const { refinements } = this.resolveRuleTarget(obligation, dataSources, policyTargetId);
364
+ const obligationTargetLookup = this.tryResolveTargetDataSource(this.getTargetId(obligation.target), dataSources, true, policyTargetId);
365
+ const ruleDataContext = obligationTargetLookup.value;
366
+ const constraints = [
367
+ ...ArrayHelper.fromObjectOrArray(obligation.constraint ?? []),
368
+ ...refinements
369
+ ];
370
+ if (constraints.length > 0 &&
371
+ !constraints.every(c => this.evaluateConstraint(c, dataSources))) {
372
+ return true;
373
+ }
374
+ return this.enforceDuty(policy, obligation, dataSources, ruleDataContext);
375
+ }
376
+ /**
377
+ * Merge inherited policies into the current policy.
378
+ * Inherited policies' permissions, prohibitions, and obligations are combined with the current policy's rules.
379
+ * @param policy The policy to evaluate.
380
+ * @returns A new policy with merged rules from all ancestors.
381
+ * @internal
382
+ */
383
+ async mergeInheritedPolicies(policy) {
384
+ const visitedPolicyIds = [];
385
+ visitedPolicyIds.push(OdrlPolicyHelper.getUid(policy) ?? "");
386
+ const inheritedPolicies = await this.resolveInheritedPolicies(policy, visitedPolicyIds, 0);
387
+ const conflictStrategies = new Set();
388
+ if (Is.stringValue(policy.conflict)) {
389
+ conflictStrategies.add(policy.conflict);
390
+ }
391
+ // Start with copies of the current policy's rules
392
+ const mergedPermissions = ArrayHelper.fromObjectOrArray(policy.permission ?? []).map(permission => this.applyPolicyDefaultsToRule(policy, permission));
393
+ const mergedProhibitions = ArrayHelper.fromObjectOrArray(policy.prohibition ?? []).map(prohibition => this.applyPolicyDefaultsToRule(policy, prohibition));
394
+ const mergedObligations = ArrayHelper.fromObjectOrArray(policy.obligation ?? []).map(obligation => this.applyPolicyDefaultsToRule(policy, obligation));
395
+ // Merge rules from each inherited policy, applying the same profile guard as the
396
+ // top-level policy. A parent that declares an unsupported profile may carry rules
397
+ // whose semantics the arbiter cannot guarantee, so it is rejected.
398
+ for (const inheritedPolicy of inheritedPolicies) {
399
+ const inheritedProfiles = ArrayHelper.fromObjectOrArray(inheritedPolicy.profile ?? [])
400
+ .filter(p => Is.stringValue(p))
401
+ .map(p => DefaultPolicyArbiter.normalizeProfileIri(p));
402
+ if (Is.arrayValue(inheritedProfiles) &&
403
+ !inheritedProfiles.every(p => DefaultPolicyArbiter.SUPPORTED_PROFILES.has(p))) {
404
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "inheritedPolicyProfileNotSupported", { policyId: OdrlPolicyHelper.getUid(inheritedPolicy) ?? "" });
405
+ }
406
+ if (Is.stringValue(inheritedPolicy.conflict)) {
407
+ conflictStrategies.add(inheritedPolicy.conflict);
408
+ }
409
+ const inheritedPermissions = ArrayHelper.fromObjectOrArray(inheritedPolicy.permission ?? []);
410
+ if (inheritedPermissions.length > 0) {
411
+ mergedPermissions.push(...inheritedPermissions.map(permission => this.applyPolicyDefaultsToRule(inheritedPolicy, permission)));
412
+ }
413
+ const inheritedProhibitions = ArrayHelper.fromObjectOrArray(inheritedPolicy.prohibition ?? []);
414
+ if (inheritedProhibitions.length > 0) {
415
+ mergedProhibitions.push(...inheritedProhibitions.map(prohibition => this.applyPolicyDefaultsToRule(inheritedPolicy, prohibition)));
416
+ }
417
+ const inheritedObligations = ArrayHelper.fromObjectOrArray(inheritedPolicy.obligation ?? []);
418
+ if (inheritedObligations.length > 0) {
419
+ mergedObligations.push(...inheritedObligations.map(obligation => this.applyPolicyDefaultsToRule(inheritedPolicy, obligation)));
420
+ }
421
+ }
422
+ let mergedConflict;
423
+ if (conflictStrategies.size === 1) {
424
+ mergedConflict = Array.from(conflictStrategies)[0];
425
+ }
426
+ else if (conflictStrategies.size > 1) {
427
+ mergedConflict = OdrlConflictStrategyType.Invalid;
428
+ }
429
+ // Return a new policy with merged rules
430
+ return {
431
+ ...policy,
432
+ conflict: mergedConflict,
433
+ permission: mergedPermissions.length > 0 ? mergedPermissions : undefined,
434
+ prohibition: mergedProhibitions.length > 0 ? mergedProhibitions : undefined,
435
+ obligation: mergedObligations.length > 0 ? mergedObligations : undefined
436
+ };
437
+ }
438
+ /**
439
+ * Resolve inherited policies by their UIDs from the Policy Administration Point.
440
+ * Policies can inherit from other policies via the inheritFrom property.
441
+ * @param policy The policy that may have inheritFrom references.
442
+ * @param visitedPolicyIds Array of policy UIDs already visited in this inheritance chain.
443
+ * @param currentDepth The current inheritance depth.
444
+ * @returns Array of inherited policies fetched from the PAP.
445
+ * @internal
446
+ */
447
+ async resolveInheritedPolicies(policy, visitedPolicyIds, currentDepth) {
448
+ const inheritedPolicies = [];
449
+ // If policy has no inheritFrom, return empty array
450
+ if (Is.empty(policy.inheritFrom)) {
451
+ return inheritedPolicies;
452
+ }
453
+ const inheritFromIds = ArrayHelper.fromObjectOrArray(policy.inheritFrom) ?? [];
454
+ for (const inheritFromId of inheritFromIds) {
455
+ const nextDepth = currentDepth + 1;
456
+ if (nextDepth > this._maxInheritanceDepth) {
457
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "maxInheritanceDepthExceeded", {
458
+ policyId: OdrlPolicyHelper.getUid(policy) ?? "",
459
+ inheritFromId,
460
+ maxInheritanceDepth: this._maxInheritanceDepth
461
+ });
462
+ }
463
+ // Check for circular inheritance
464
+ if (visitedPolicyIds.includes(inheritFromId)) {
465
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "circularInheritanceDetected", {
466
+ policyId: OdrlPolicyHelper.getUid(policy) ?? "",
467
+ inheritFromId
468
+ });
469
+ }
470
+ // Fetch the inherited policy from the PAP
471
+ const inheritedPolicy = await this._policyAdministrationPoint.get(inheritFromId);
472
+ if (Is.empty(inheritedPolicy)) {
473
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "inheritedPolicyNotFound", {
474
+ inheritFromId
475
+ });
476
+ }
477
+ // Mark this policy as visited
478
+ visitedPolicyIds.push(inheritFromId);
479
+ inheritedPolicies.push(inheritedPolicy);
480
+ // Recursively resolve inherited policies of the parent
481
+ const grandparentPolicies = await this.resolveInheritedPolicies(inheritedPolicy, visitedPolicyIds, nextDepth);
482
+ inheritedPolicies.push(...grandparentPolicies);
483
+ }
484
+ return inheritedPolicies;
485
+ }
486
+ /**
487
+ * Apply policy-level default values (assigner, assignee, target, action) to rules that don't override them.
488
+ * @param policy The policy providing defaults.
489
+ * @param rule The rule to apply defaults to.
490
+ * @returns The rule with policy-level defaults applied.
491
+ * @internal
492
+ */
493
+ applyPolicyDefaultsToRule(policy, rule) {
494
+ const assigner = Is.empty(rule.assigner) ? policy.assigner : rule.assigner;
495
+ const assignee = Is.empty(rule.assignee) ? policy.assignee : rule.assignee;
496
+ const target = Is.empty(rule.target) ? policy.target : rule.target;
497
+ const action = Is.empty(rule.action) ? policy.action : rule.action;
498
+ const assignerIds = OdrlPolicyHelper.getPartyIds(assigner);
499
+ const ruleAssignerIds = OdrlPolicyHelper.getPartyIds(rule.assigner);
500
+ const assigneeIds = OdrlPolicyHelper.getPartyIds(assignee);
501
+ const ruleAssigneeIds = OdrlPolicyHelper.getPartyIds(rule.assignee);
502
+ let assignerEqual = false;
503
+ if (Is.empty(assignerIds) && Is.empty(ruleAssignerIds)) {
504
+ assignerEqual = true;
505
+ }
506
+ else if (!Is.empty(assignerIds) && !Is.empty(ruleAssignerIds)) {
507
+ assignerEqual =
508
+ assignerIds.length === ruleAssignerIds.length &&
509
+ assignerIds.every(id => ruleAssignerIds.includes(id));
510
+ }
511
+ let assigneeEqual = false;
512
+ if (Is.empty(assigneeIds) && Is.empty(ruleAssigneeIds)) {
513
+ assigneeEqual = true;
514
+ }
515
+ else if (!Is.empty(assigneeIds) && !Is.empty(ruleAssigneeIds)) {
516
+ assigneeEqual =
517
+ assigneeIds.length === ruleAssigneeIds.length &&
518
+ assigneeIds.every(id => ruleAssigneeIds.includes(id));
519
+ }
520
+ const targetEqual = target === rule.target;
521
+ const actionEqual = action === rule.action;
522
+ if (assignerEqual && assigneeEqual && targetEqual && actionEqual) {
523
+ return rule;
524
+ }
525
+ return {
526
+ ...rule,
527
+ assigner,
528
+ assignee,
529
+ target,
530
+ action
531
+ };
532
+ }
533
+ /**
534
+ * Determine whether a rule applies to the agreement parties based on assigner/assignee.
535
+ * @param rule The rule to evaluate.
536
+ * @param agreementAssigner The assigner ID from the agreement.
537
+ * @param agreementAssignee The assignee ID from the agreement.
538
+ * @returns True if the rule is applicable to the agreement parties.
539
+ * @internal
540
+ */
541
+ isRuleApplicableToParties(rule, agreementAssigner, agreementAssignee, dataSources) {
542
+ const assignerContext = this.resolveRulePartyContext(rule.assigner);
543
+ const assigneeContext = this.resolveRulePartyContext(rule.assignee);
544
+ if (!this.isPartyContextApplicable(assignerContext, agreementAssigner, dataSources)) {
545
+ return false;
546
+ }
547
+ if (!this.isPartyContextApplicable(assigneeContext, agreementAssignee, dataSources)) {
548
+ return false;
549
+ }
550
+ return true;
551
+ }
552
+ /**
553
+ * Determine whether a resolved party context (ids and/or refinements) applies to an agreement party.
554
+ * A refinement-bearing context with no resolvable ids (e.g. a source-less PartyCollection) is decided
555
+ * by evaluating its refinements instead of failing on the empty id list. A context with neither ids
556
+ * nor refinements (e.g. a malformed/untyped party entry) is NOT treated as "no constraint" here - it
557
+ * falls through to isPartyApplicable, which keeps the pre-existing fail-closed behaviour (denies,
558
+ * since an empty id list never matches). An id-bearing context keeps matching by id, ANDing in any
559
+ * refinements exactly as before.
560
+ * @param context The resolved party context (ids and refinements) from the rule.
561
+ * @param context.partyIds The party ids resolved from the rule (empty when none are resolvable).
562
+ * @param context.refinements The refinement constraints resolved from the rule.
563
+ * @param agreementPartyIds The party ids extracted from the agreement.
564
+ * @param dataSources The operand lookup sources for refinement evaluation.
565
+ * @returns True if the party context is satisfied.
566
+ * @internal
567
+ */
568
+ isPartyContextApplicable(context, agreementPartyIds, dataSources) {
569
+ if (context.partyIds.length === 0 && context.refinements.length > 0) {
570
+ return context.refinements.every(c => this.evaluateConstraint(c, dataSources));
571
+ }
572
+ if (!this.isPartyApplicable(context.partyIds, agreementPartyIds)) {
573
+ return false;
574
+ }
575
+ if (context.refinements.length > 0 &&
576
+ !context.refinements.every(c => this.evaluateConstraint(c, dataSources))) {
577
+ return false;
578
+ }
579
+ return true;
580
+ }
581
+ /**
582
+ * Resolve rule party identifiers and refinements. A refinement-only PartyCollection (no source)
583
+ * contributes no ids and is matched by evaluating its refinements instead; PartyCollection source
584
+ * values remain unsupported for party matching and throw.
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
+ * @param action Optional action to check against the permission's applicable actions.
751
+ * @param decisionTarget Optional target identifier for scoped decisions.
752
+ * @returns True if the permission applies.
753
+ * @internal
754
+ */
755
+ async evaluatePermission(agreementAssigner, agreementAssignee, policy, permission, targetRefinements, dataSources, action, decisionTarget) {
756
+ if (!this.isRuleApplicableToParties(permission, agreementAssigner, agreementAssignee, dataSources)) {
757
+ return false;
758
+ }
759
+ // Check if the permission's action(s) match the requested action
760
+ if (!this.isActionApplicable(permission.action, action, dataSources)) {
761
+ return false;
762
+ }
763
+ // ODRL semantics: a Permission without constraints is unconditional.
764
+ const constraints = [
765
+ ...ArrayHelper.fromObjectOrArray(permission.constraint ?? []),
766
+ ...targetRefinements
767
+ ];
768
+ const permissionTargetLookup = this.tryResolveTargetDataSource(this.buildRuleDataContextTargetId(this.getRuleDataContextTargetId(permission.target), decisionTarget), dataSources, true, this.tryGetPolicyAssetTargetId(policy));
769
+ const ruleDataContext = permissionTargetLookup.value;
770
+ if (constraints.length === 0) {
771
+ return this.enforcePermissionDuties(policy, permission, dataSources, ruleDataContext);
772
+ }
773
+ // All constraints must be satisfied for the permission to apply.
774
+ const constraintsSatisfied = constraints.every(c => this.evaluateConstraint(c, dataSources));
775
+ if (!constraintsSatisfied) {
776
+ return false;
777
+ }
778
+ return this.enforcePermissionDuties(policy, permission, dataSources, ruleDataContext);
779
+ }
780
+ /**
781
+ * Enforce duties attached to a permission.
782
+ * @param policy The policy being evaluated.
783
+ * @param permission The permission being evaluated.
784
+ * @param dataSources The operand lookup sources.
785
+ * @param ruleDataContext The target-scoped data context passed to enforcers.
786
+ * @returns True if all duties are enforced or none are present.
787
+ * @internal
788
+ */
789
+ async enforcePermissionDuties(policy, permission, dataSources, ruleDataContext) {
790
+ const duties = ArrayHelper.fromObjectOrArray(permission.duty ?? []);
791
+ if (duties.length === 0) {
792
+ return true;
793
+ }
794
+ for (const duty of duties) {
795
+ const enforced = await this.enforceDuty(policy, duty, dataSources, ruleDataContext);
796
+ if (!enforced) {
797
+ return false;
798
+ }
799
+ }
800
+ return true;
801
+ }
802
+ /**
803
+ * Enforce a single duty using registered obligation enforcers.
804
+ * @param policy The policy being evaluated.
805
+ * @param duty The duty to enforce.
806
+ * @param dataSources The operand lookup sources.
807
+ * @param ruleDataContext The target-scoped data context passed to enforcers.
808
+ * @returns True if any enforcer succeeds.
809
+ * @internal
810
+ */
811
+ async enforceDuty(policy, duty, dataSources, ruleDataContext) {
812
+ const enforcerNames = PolicyObligationEnforcerFactory.names();
813
+ const information = dataSources[OdrlTwinVocabulary.InformationSourceKey];
814
+ if (enforcerNames.length === 0) {
815
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "noObligationEnforcersRegistered");
816
+ }
817
+ for (const enforcerName of enforcerNames) {
818
+ const enforcer = PolicyObligationEnforcerFactory.get(enforcerName);
819
+ if (await enforcer.enforce(policy, duty, information, ruleDataContext)) {
820
+ return true;
821
+ }
822
+ }
823
+ const consequences = ArrayHelper.fromObjectOrArray(duty.consequence ?? []);
824
+ if (consequences.length === 0) {
825
+ return false;
826
+ }
827
+ for (const consequence of consequences) {
828
+ if (!(await this.enforceDuty(policy, consequence, dataSources, ruleDataContext))) {
829
+ return false;
830
+ }
831
+ }
832
+ return true;
833
+ }
834
+ /**
835
+ * Resolve a target string to a matching datasource prefix and remaining target value.
836
+ * @param targetId The target identifier to resolve.
837
+ * @param dataSources The available lookup sources.
838
+ * @param resolveValue True to resolve an item from the target path/key.
839
+ * @param policyTargetId The policy-level asset target id, a rule target equal to it scopes to the whole payload.
840
+ * @returns The matching prefix, source, remaining target and optional resolved value.
841
+ * @throws GeneralError if the target identifier does not match any datasource prefix.
842
+ * @internal
843
+ */
844
+ tryResolveTargetDataSource(targetId, dataSources, resolveValue = false, policyTargetId) {
845
+ // If there is no target id, default to the data datasource
846
+ if (Is.empty(targetId)) {
847
+ return {
848
+ source: dataSources[OdrlTwinVocabulary.DataSourceKey],
849
+ target: "$",
850
+ value: dataSources[OdrlTwinVocabulary.DataSourceKey]
851
+ };
852
+ }
853
+ // Handle twin:jsonPath:<datasource>:<expression> format.
854
+ // The datasource segment is optional; when absent the expression starts with "$"
855
+ // and falls back to the primary data source.
856
+ if (targetId.startsWith(`${OdrlTwinVocabulary.JsonPath}:`)) {
857
+ const rest = targetId.slice(OdrlTwinVocabulary.JsonPath.length + 1);
858
+ const matchingKey = Object.keys(dataSources).find(k => rest.startsWith(`${k}:`));
859
+ const sourceKey = matchingKey ?? OdrlTwinVocabulary.DataSourceKey;
860
+ const expression = matchingKey ? rest.slice(matchingKey.length + 1) : rest;
861
+ return this.resolveDataSourceByKey(sourceKey, expression, dataSources, resolveValue);
862
+ }
863
+ // Handle <datasource>:<expression> format
864
+ const matchingKey = Object.keys(dataSources).find(k => targetId.startsWith(`${k}:`));
865
+ if (matchingKey) {
866
+ const expression = targetId.slice(matchingKey.length + 1);
867
+ return this.resolveDataSourceByKey(matchingKey, expression, dataSources, resolveValue);
868
+ }
869
+ // A rule target equal to the policy-level asset id means the whole asset under
870
+ // evaluation, i.e. the entire data payload (negotiated agreements put the dataset
871
+ // id as the policy target and untargeted rules inherit it). Kept at the fallthrough
872
+ // so grammar-resolvable forms keep their scoping and unrelated asset ids still throw.
873
+ if (Is.stringValue(policyTargetId) && targetId === policyTargetId) {
874
+ return {
875
+ source: dataSources[OdrlTwinVocabulary.DataSourceKey],
876
+ target: "$",
877
+ value: dataSources[OdrlTwinVocabulary.DataSourceKey]
878
+ };
879
+ }
880
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
881
+ target: targetId
882
+ });
883
+ }
884
+ /**
885
+ * Resolve a datasource by key and evaluate a JSONPath expression against it.
886
+ * @param sourceKey The datasource key.
887
+ * @param expression The JSONPath expression.
888
+ * @param dataSources The available datasources.
889
+ * @param resolveValue Whether to evaluate the expression and return the matched value.
890
+ * @returns The resolved source, target expression, and optionally the matched value.
891
+ * @throws GeneralError if the expression is missing, does not start with "$", or yields no matches.
892
+ * @internal
893
+ */
894
+ resolveDataSourceByKey(sourceKey, expression, dataSources, resolveValue = false) {
895
+ if (!Is.stringValue(expression)) {
896
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
897
+ operand: "target"
898
+ });
899
+ }
900
+ if (!expression.startsWith("$")) {
901
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
902
+ target: `${sourceKey}:${expression}`
903
+ });
904
+ }
905
+ const source = dataSources[sourceKey];
906
+ if (!resolveValue) {
907
+ return { source, target: expression };
908
+ }
909
+ const matches = JsonPathHelper.query(expression, source);
910
+ if (matches.length === 0) {
911
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
912
+ target: `${sourceKey}:${expression}`
913
+ });
914
+ }
915
+ return {
916
+ source,
917
+ target: expression,
918
+ value: matches.length === 1 ? matches[0].value : matches.map(m => m.value)
919
+ };
920
+ }
921
+ /**
922
+ * Extract the target id from the permission.
923
+ * @param target The permission target.
924
+ * @returns The information key, or undefined when the target is not an information reference.
925
+ * @throws GeneralError if more than one target is specified.
926
+ * @internal
927
+ */
928
+ getTargetId(target) {
929
+ if (Is.undefined(target)) {
930
+ return undefined;
931
+ }
932
+ if (Is.array(target) && target.length > 1) {
933
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "multipleTargetsNotSupported");
934
+ }
935
+ const arr = ArrayHelper.fromObjectOrArray(target ?? []);
936
+ if (arr.length === 0) {
937
+ return undefined;
938
+ }
939
+ const first = arr[0];
940
+ if (Is.string(first)) {
941
+ return first;
942
+ }
943
+ if (this.isTwinJsonPathTarget(first)) {
944
+ const t = first;
945
+ const expression = t[OdrlTwinVocabulary.JsonPathExpression];
946
+ if (!Is.stringValue(expression)) {
947
+ return undefined;
948
+ }
949
+ const dataSource = t[OdrlTwinVocabulary.JsonPathDataSource];
950
+ const sourceKey = Is.stringValue(dataSource) ? dataSource : OdrlTwinVocabulary.DataSourceKey;
951
+ return `${sourceKey}:${expression}`;
952
+ }
953
+ return OdrlPolicyHelper.getUid(first);
954
+ }
955
+ /**
956
+ * Get the policy-level target id when it is a single plain asset reference.
957
+ * Returns undefined for absent, multiple, or twin:jsonPath targets, never throws.
958
+ * @param policy The policy to read the target from.
959
+ * @returns The asset id, or undefined when the policy target is not a single plain asset.
960
+ * @internal
961
+ */
962
+ tryGetPolicyAssetTargetId(policy) {
963
+ const arr = ArrayHelper.fromObjectOrArray(policy.target ?? []);
964
+ if (arr.length !== 1) {
965
+ return undefined;
966
+ }
967
+ const first = arr[0];
968
+ if (Is.string(first)) {
969
+ return first;
970
+ }
971
+ if (this.isTwinJsonPathTarget(first)) {
972
+ return undefined;
973
+ }
974
+ return OdrlPolicyHelper.getUid(first);
975
+ }
976
+ /**
977
+ * Resolve the target identifier used for rule data context lookup.
978
+ * For AssetCollection targets the `source` property is used because the collection has no `uid`.
979
+ * Falls back to `getTargetId` for all other target forms.
980
+ * @param target The rule target field value.
981
+ * @returns The prefixed target string for data context resolution.
982
+ * @internal
983
+ */
984
+ getRuleDataContextTargetId(target) {
985
+ const arr = ArrayHelper.fromObjectOrArray(target ?? []);
986
+ if (arr.length === 1) {
987
+ const firstTarget = arr[0];
988
+ if (Is.object(firstTarget) &&
989
+ OdrlPolicyHelper.getType(firstTarget) === OdrlTypes.AssetCollection &&
990
+ firstTarget.source === OdrlTwinVocabulary.JsonPath) {
991
+ const ctx = firstTarget;
992
+ const expression = ctx[OdrlTwinVocabulary.JsonPathExpression];
993
+ if (Is.stringValue(expression)) {
994
+ const dataSource = ctx[OdrlTwinVocabulary.JsonPathDataSource];
995
+ const sourceKey = Is.stringValue(dataSource)
996
+ ? dataSource
997
+ : OdrlTwinVocabulary.DataSourceKey;
998
+ return `${OdrlTwinVocabulary.JsonPath}:${sourceKey}:${expression}`;
999
+ }
1000
+ }
1001
+ }
1002
+ return this.getTargetId(target);
1003
+ }
1004
+ /**
1005
+ * Build a concrete prefixed target id for rule data-context lookup.
1006
+ * @param baseTargetId The original prefixed target id from the rule.
1007
+ * @param decisionTarget The concrete decision JSONPath target.
1008
+ * @returns The concrete prefixed target id.
1009
+ * @internal
1010
+ */
1011
+ buildRuleDataContextTargetId(baseTargetId, decisionTarget) {
1012
+ if (!Is.stringValue(decisionTarget) || decisionTarget === "$") {
1013
+ return baseTargetId;
1014
+ }
1015
+ if (!Is.stringValue(baseTargetId)) {
1016
+ return undefined;
1017
+ }
1018
+ const pathStartIndex = baseTargetId.indexOf(":$");
1019
+ if (pathStartIndex < 0) {
1020
+ return baseTargetId;
1021
+ }
1022
+ return `${baseTargetId.slice(0, pathStartIndex)}:${decisionTarget}`;
1023
+ }
1024
+ /**
1025
+ * Resolve a rule target into a policy-decision JSONPath target and extracted refinements.
1026
+ * For AssetCollection targets, `source` is treated as the decision target and `refinement`
1027
+ * constraints are applied as additional rule constraints.
1028
+ * @param rule The rule to resolve the target for.
1029
+ * @param dataSources The operand lookup sources.
1030
+ * @param policyTargetId The policy-level asset target id, a rule target equal to it scopes to the whole payload.
1031
+ * @returns The decision target and target refinements.
1032
+ * @throws GeneralError if target is invalid or unsupported.
1033
+ * @internal
1034
+ */
1035
+ resolveRuleTarget(rule, dataSources, policyTargetId) {
1036
+ const arr = ArrayHelper.fromObjectOrArray(rule.target ?? []);
1037
+ if (arr.length === 0) {
1038
+ return {
1039
+ target: "$",
1040
+ refinements: []
1041
+ };
1042
+ }
1043
+ if (arr.length > 1) {
1044
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "multipleTargetsNotSupported");
1045
+ }
1046
+ const firstTarget = arr[0];
1047
+ if (this.isTwinJsonPathTarget(firstTarget)) {
1048
+ const t = firstTarget;
1049
+ const resolved = this.resolveDataSourceByKey(t[OdrlTwinVocabulary.JsonPathDataSource] ?? OdrlTwinVocabulary.DataSourceKey, t[OdrlTwinVocabulary.JsonPathExpression], dataSources);
1050
+ return {
1051
+ target: resolved.target,
1052
+ refinements: []
1053
+ };
1054
+ }
1055
+ if (Is.object(firstTarget)) {
1056
+ // Guard against unsupported ODRL asset properties
1057
+ if (Is.notEmpty(firstTarget.hasPolicy)) {
1058
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetHasPolicyNotSupported");
1059
+ }
1060
+ if (Is.notEmpty(firstTarget.partOf)) {
1061
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetPartOfNotSupported");
1062
+ }
1063
+ if (Is.object(firstTarget) &&
1064
+ OdrlPolicyHelper.getType(firstTarget) === OdrlTypes.AssetCollection) {
1065
+ if (!Is.stringValue(firstTarget.source)) {
1066
+ return {
1067
+ target: "$",
1068
+ refinements: ArrayHelper.fromObjectOrArray(firstTarget.refinement ?? [])
1069
+ };
1070
+ }
1071
+ if (firstTarget.source !== OdrlTwinVocabulary.JsonPath) {
1072
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetCollectionSourceNotSupported", {
1073
+ source: firstTarget.source ?? ""
1074
+ });
1075
+ }
1076
+ const ctx = firstTarget;
1077
+ const dataSource = ctx[OdrlTwinVocabulary.JsonPathDataSource];
1078
+ const sourceKey = Is.stringValue(dataSource)
1079
+ ? dataSource
1080
+ : OdrlTwinVocabulary.DataSourceKey;
1081
+ let sourceLookup;
1082
+ try {
1083
+ sourceLookup = this.resolveDataSourceByKey(sourceKey, ctx[OdrlTwinVocabulary.JsonPathExpression], dataSources);
1084
+ }
1085
+ catch {
1086
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "assetCollectionSourceNotSupported", {
1087
+ source: ctx[OdrlTwinVocabulary.JsonPathExpression] ?? ""
1088
+ });
1089
+ }
1090
+ return {
1091
+ target: sourceLookup.target,
1092
+ refinements: ArrayHelper.fromObjectOrArray(firstTarget.refinement ?? [])
1093
+ };
1094
+ }
1095
+ }
1096
+ const targetId = Is.string(firstTarget) ? firstTarget : OdrlPolicyHelper.getUid(firstTarget);
1097
+ if (!Is.stringValue(targetId)) {
1098
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "ruleTargetNotSupported", {
1099
+ target: ""
1100
+ });
1101
+ }
1102
+ const targetLookup = this.tryResolveTargetDataSource(targetId, dataSources, false, policyTargetId);
1103
+ return {
1104
+ target: targetLookup.target,
1105
+ refinements: []
1106
+ };
1107
+ }
1108
+ /**
1109
+ * Resolve decision targets for a rule.
1110
+ * AssetCollection wildcard targets with refinements are expanded to per-item targets.
1111
+ * @param rule The rule being evaluated.
1112
+ * @param dataSources The operand lookup sources.
1113
+ * @param policyTargetId The policy-level asset target id, a rule target equal to it scopes to the whole payload.
1114
+ * @returns The decision targets and scoped refinements.
1115
+ * @internal
1116
+ */
1117
+ resolveRuleDecisionTargets(rule, dataSources, policyTargetId) {
1118
+ const resolvedTarget = this.resolveRuleTarget(rule, dataSources, policyTargetId);
1119
+ if (!this.shouldExpandToPerItemTargets(rule, resolvedTarget)) {
1120
+ return [resolvedTarget];
1121
+ }
1122
+ const sourceLookup = this.tryResolveTargetDataSource(this.getRuleDataContextTargetId(rule.target), dataSources);
1123
+ const matches = JsonPathHelper.query(sourceLookup.target, sourceLookup.source);
1124
+ if (matches.length === 0) {
1125
+ // Distinguish "array exists but is empty" from "path not found in source".
1126
+ // An empty array yields no per-item targets; the closed-world fallback then
1127
+ // handles the decision rather than letting evaluatePermission throw on [*].
1128
+ if (sourceLookup.target.includes("[*]")) {
1129
+ const arrayPath = sourceLookup.target.split("[*]")[0];
1130
+ const parentMatches = JsonPathHelper.query(arrayPath, sourceLookup.source);
1131
+ if (parentMatches.length === 1 &&
1132
+ Is.array(parentMatches[0].value) &&
1133
+ parentMatches[0].value.length === 0) {
1134
+ return [];
1135
+ }
1136
+ }
1137
+ return [resolvedTarget];
1138
+ }
1139
+ return matches.map(match => {
1140
+ const itemTarget = this.normalizeDecisionTargetPath(match.path ?? resolvedTarget.target);
1141
+ return {
1142
+ target: itemTarget,
1143
+ refinements: resolvedTarget.refinements.map(refinement => this.rewriteRefinementForDecisionTarget(refinement, resolvedTarget.target, itemTarget))
1144
+ };
1145
+ });
1146
+ }
1147
+ /**
1148
+ * Determine if a rule should be expanded to per-item targets.
1149
+ * @param rule The rule.
1150
+ * @param resolvedTarget The resolved target details.
1151
+ * @param resolvedTarget.target The resolved target path string.
1152
+ * @param resolvedTarget.refinements The constraints from target refinement.
1153
+ * @returns True if the rule should emit per-item decisions.
1154
+ * @internal
1155
+ */
1156
+ shouldExpandToPerItemTargets(rule, resolvedTarget) {
1157
+ if (resolvedTarget.refinements.length === 0 || !resolvedTarget.target.includes("[*]")) {
1158
+ return false;
1159
+ }
1160
+ const targets = ArrayHelper.fromObjectOrArray(rule.target ?? []);
1161
+ if (targets.length !== 1 || !Is.object(targets[0])) {
1162
+ return false;
1163
+ }
1164
+ return OdrlPolicyHelper.getType(targets[0]) === OdrlTypes.AssetCollection;
1165
+ }
1166
+ /**
1167
+ * Rewrite a refinement so wildcard paths are scoped to a concrete item target.
1168
+ * @param refinement The refinement to rewrite.
1169
+ * @param sourceTarget The wildcard source target.
1170
+ * @param itemTarget The concrete item target.
1171
+ * @returns The rewritten refinement.
1172
+ * @internal
1173
+ */
1174
+ rewriteRefinementForDecisionTarget(refinement, sourceTarget, itemTarget) {
1175
+ const logicalConstraint = this.getLogicalConstraintOperands(refinement);
1176
+ if (logicalConstraint) {
1177
+ return {
1178
+ ...refinement,
1179
+ [logicalConstraint.operator]: logicalConstraint.constraints.map(item => this.rewriteRefinementForDecisionTarget(item, sourceTarget, itemTarget))
1180
+ };
1181
+ }
1182
+ const regularConstraint = refinement;
1183
+ const constraintWithExtensions = regularConstraint;
1184
+ const canonicalExpression = constraintWithExtensions[OdrlTwinVocabulary.JsonPathExpression];
1185
+ const rewrittenConstraint = {
1186
+ ...regularConstraint,
1187
+ leftOperand: this.rewriteOperandForDecisionTarget(regularConstraint.leftOperand, sourceTarget, itemTarget),
1188
+ rightOperand: this.rewriteOperandForDecisionTarget(regularConstraint.rightOperand, sourceTarget, itemTarget)
1189
+ };
1190
+ if (Is.stringValue(canonicalExpression)) {
1191
+ rewrittenConstraint[OdrlTwinVocabulary.JsonPathExpression] = this.rewriteWildcardPath(canonicalExpression, sourceTarget, itemTarget);
1192
+ }
1193
+ return rewrittenConstraint;
1194
+ }
1195
+ /**
1196
+ * Rewrite JSONPath-based operands from wildcard source to concrete item target.
1197
+ * @param operand The operand to rewrite.
1198
+ * @param sourceTarget The wildcard source target.
1199
+ * @param itemTarget The concrete item target.
1200
+ * @returns The rewritten operand.
1201
+ * @internal
1202
+ */
1203
+ rewriteOperandForDecisionTarget(operand, sourceTarget, itemTarget) {
1204
+ if (Is.object(operand)) {
1205
+ const typedOperand = {
1206
+ ...operand
1207
+ };
1208
+ if (this.isTwinJsonPathOperandType(typedOperand["@type"])) {
1209
+ if (Is.stringValue(typedOperand["@value"])) {
1210
+ typedOperand["@value"] = this.rewriteWildcardPath(typedOperand["@value"], sourceTarget, itemTarget);
1211
+ }
1212
+ const canonicalExpression = typedOperand[OdrlTwinVocabulary.JsonPathExpression];
1213
+ if (Is.stringValue(canonicalExpression)) {
1214
+ typedOperand[OdrlTwinVocabulary.JsonPathExpression] = this.rewriteWildcardPath(canonicalExpression, sourceTarget, itemTarget);
1215
+ }
1216
+ }
1217
+ return typedOperand;
1218
+ }
1219
+ // Legacy string form: "twin:information:$.foo". Extract the prefix, rewrite the
1220
+ // JSONPath expression to scope to the per-item target, then re-assemble. Without
1221
+ // this, the leftOperand stays at the wildcard
1222
+ // `$.itemList.itemListElement[*].unloadingLocation.id` for every item, returning
1223
+ // the full array of all ids per check - so all items pass the equality test.
1224
+ if (Is.stringValue(operand) &&
1225
+ operand.startsWith(`twin:${OdrlTwinVocabulary.InformationSourceKey}:`)) {
1226
+ const prefix = `twin:${OdrlTwinVocabulary.InformationSourceKey}:`;
1227
+ const expression = operand.slice(prefix.length);
1228
+ const rewritten = this.rewriteWildcardPath(expression, sourceTarget, itemTarget);
1229
+ return `${prefix}${rewritten}`;
1230
+ }
1231
+ return operand;
1232
+ }
1233
+ /**
1234
+ * Rewrite wildcard source JSONPath segments to a concrete item JSONPath.
1235
+ * @param valuePath The operand path.
1236
+ * @param sourceTarget The wildcard source path.
1237
+ * @param itemTarget The concrete item path.
1238
+ * @returns The rewritten path.
1239
+ * @internal
1240
+ */
1241
+ rewriteWildcardPath(valuePath, sourceTarget, itemTarget) {
1242
+ if (!sourceTarget.includes("[*]") || !valuePath.includes("[*]")) {
1243
+ return valuePath;
1244
+ }
1245
+ if (valuePath.startsWith(sourceTarget)) {
1246
+ return `${itemTarget}${valuePath.slice(sourceTarget.length)}`;
1247
+ }
1248
+ return valuePath;
1249
+ }
1250
+ /**
1251
+ * Normalize JSONPath strings to dot notation for stable decision targets.
1252
+ * @param path The JSONPath to normalize.
1253
+ * @returns The normalized path.
1254
+ * @internal
1255
+ */
1256
+ normalizeDecisionTargetPath(path) {
1257
+ return path.replace(/\['([^']+)']/g, ".$1");
1258
+ }
1259
+ /**
1260
+ * Evaluate a single ODRL constraint against the available context.
1261
+ * Supports logical constraint composition through nested refinements.
1262
+ * @param constraint The constraint to evaluate.
1263
+ * @param dataSources The operand lookup sources.
1264
+ * @returns True if the constraint is satisfied.
1265
+ * @throws GeneralError if rightOperandReference, dataType, unit, or status are used.
1266
+ * @internal
1267
+ */
1268
+ evaluateConstraint(constraint, dataSources) {
1269
+ const logicalConstraint = this.getLogicalConstraintOperands(constraint);
1270
+ if (logicalConstraint) {
1271
+ return this.evaluateLogicalConstraint(logicalConstraint, dataSources);
1272
+ }
1273
+ // Must be a regular constraint beyond this point
1274
+ const regularConstraint = constraint;
1275
+ // rightOperandReference is not supported - it requires an external IRI lookup that
1276
+ // is outside the scope of the local evaluation engine.
1277
+ if (Is.notEmpty(regularConstraint.rightOperandReference)) {
1278
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "rightOperandReferenceNotSupported");
1279
+ }
1280
+ // dataType specifies how the rightOperand value should be coerced before comparison.
1281
+ // Without dataType-aware coercion logic the comparison may produce incorrect results.
1282
+ if (Is.notEmpty(regularConstraint.dataType)) {
1283
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintDataTypeNotSupported");
1284
+ }
1285
+ // unit specifies the measurement unit for the right operand (e.g. currency, length).
1286
+ // Unit-aware comparison is not implemented.
1287
+ if (Is.notEmpty(regularConstraint.unit)) {
1288
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintUnitNotSupported");
1289
+ }
1290
+ // status represents a state-based evaluation operand (e.g. odrl:policyUsage).
1291
+ // State-based evaluation is not implemented.
1292
+ if (Is.notEmpty(regularConstraint.status)) {
1293
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "constraintStatusNotSupported");
1294
+ }
1295
+ // Evaluate the main constraint condition
1296
+ const leftValue = this.calculateOperandValue(regularConstraint.leftOperand, dataSources, regularConstraint, true);
1297
+ const rightValue = this.calculateOperandValue(regularConstraint.rightOperand, dataSources, regularConstraint, false);
1298
+ const mainSatisfied = this.evaluateOperator(regularConstraint.operator, leftValue, rightValue);
1299
+ // If main constraint is not satisfied, the overall constraint fails
1300
+ return mainSatisfied;
1301
+ }
1302
+ /**
1303
+ * Extract logical constraint operands when present.
1304
+ * @param constraint The constraint to inspect.
1305
+ * @returns The logical operator and its operands, or undefined when not logical.
1306
+ * @throws GeneralError if logical constraint operands are not unique.
1307
+ * @internal
1308
+ */
1309
+ getLogicalConstraintOperands(constraint) {
1310
+ const logicalConstraint = constraint;
1311
+ const operators = Object.values(OdrlLogicalConstraintType);
1312
+ for (const operator of operators) {
1313
+ const value = logicalConstraint[operator];
1314
+ if (!Is.undefined(value)) {
1315
+ const constraints = this.normalizeLogicalConstraintOperands(value);
1316
+ this.validateLogicalConstraintOperandUniqueness(constraints, operator);
1317
+ return {
1318
+ operator,
1319
+ constraints
1320
+ };
1321
+ }
1322
+ }
1323
+ return undefined;
1324
+ }
1325
+ /**
1326
+ * Normalize logical constraint operands into a constraint array.
1327
+ * Handles both IOdrlConstraint and IOdrlLogicalConstraint types.
1328
+ * @param raw The raw operand value.
1329
+ * @returns The constraint array.
1330
+ * @internal
1331
+ */
1332
+ normalizeLogicalConstraintOperands(raw) {
1333
+ let normalized = raw;
1334
+ if (Is.object(normalized) &&
1335
+ !Is.undefined(normalized["@list"])) {
1336
+ normalized = normalized["@list"];
1337
+ }
1338
+ return (ArrayHelper.fromObjectOrArray(normalized) ?? [])
1339
+ .filter(item => Is.object(item))
1340
+ .map(item => item);
1341
+ }
1342
+ /**
1343
+ * Validate that all operands in a logical constraint are unique.
1344
+ * ODRL spec 2.5.2 requires that all operand values MUST be unique Constraint instances.
1345
+ * Uniqueness is checked by uid property and id property.
1346
+ * @param constraints The constraint operands to validate.
1347
+ * @param operator The logical operator type (for error messaging).
1348
+ * @throws GeneralError if duplicate constraints are found.
1349
+ * @internal
1350
+ */
1351
+ validateLogicalConstraintOperandUniqueness(constraints, operator) {
1352
+ const seenIdentifiers = new Set();
1353
+ for (let i = 0; i < constraints.length; i++) {
1354
+ const constraint = constraints[i];
1355
+ const identifier = OdrlPolicyHelper.getUid(constraint);
1356
+ // If we have an identifier, check for duplicates
1357
+ if (Is.stringValue(identifier)) {
1358
+ if (seenIdentifiers.has(identifier)) {
1359
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "logicalConstraintOperandNotUnique", {
1360
+ operator,
1361
+ identifier,
1362
+ index: i
1363
+ });
1364
+ }
1365
+ seenIdentifiers.add(identifier);
1366
+ }
1367
+ }
1368
+ }
1369
+ /**
1370
+ * Evaluate a logical constraint operator against its operands.
1371
+ * @param logicalConstraint The operator and operand list.
1372
+ * @param logicalConstraint.operator The logical operator type.
1373
+ * @param logicalConstraint.constraints The constraint operands to evaluate.
1374
+ * @param dataSources The operand lookup sources.
1375
+ * @returns True if the logical constraint is satisfied.
1376
+ * @internal
1377
+ */
1378
+ evaluateLogicalConstraint(logicalConstraint, dataSources) {
1379
+ const { operator, constraints } = logicalConstraint;
1380
+ if (constraints.length === 0) {
1381
+ return false;
1382
+ }
1383
+ switch (operator) {
1384
+ case OdrlLogicalConstraintType.And:
1385
+ return constraints.every(item => this.evaluateConstraint(item, dataSources));
1386
+ case OdrlLogicalConstraintType.AndSequence: {
1387
+ for (const item of constraints) {
1388
+ if (!this.evaluateConstraint(item, dataSources)) {
1389
+ return false;
1390
+ }
1391
+ }
1392
+ return true;
1393
+ }
1394
+ case OdrlLogicalConstraintType.Or:
1395
+ return constraints.some(item => this.evaluateConstraint(item, dataSources));
1396
+ case OdrlLogicalConstraintType.Xone: {
1397
+ let satisfied = 0;
1398
+ for (const item of constraints) {
1399
+ if (this.evaluateConstraint(item, dataSources)) {
1400
+ satisfied += 1;
1401
+ if (satisfied > 1) {
1402
+ return false;
1403
+ }
1404
+ }
1405
+ }
1406
+ return satisfied === 1;
1407
+ }
1408
+ default:
1409
+ return false;
1410
+ }
1411
+ }
1412
+ /**
1413
+ * Resolve a prefixed operand to its source object and JSONPath expression.
1414
+ * Prefix matching is dictionary-driven so additional operand namespaces can be
1415
+ * added in one place by extending the lookup sources map.
1416
+ * @param operandTypeOrValue The string operand value or typed operand namespace.
1417
+ * @param operandValue The JSONPath expression.
1418
+ * @param dataSources The available lookup sources.
1419
+ * @returns The resolved source and JSONPath, or undefined when not namespaced.
1420
+ * @throws GeneralError if a twin: prefixed operand doesn't resolve to any available datasource key.
1421
+ * @internal
1422
+ */
1423
+ tryResolveOperandLookup(operandTypeOrValue, operandValue, dataSources) {
1424
+ let sourceKey = this.normalizeTwinJsonPathOperandAlias(operandTypeOrValue);
1425
+ let value = operandValue;
1426
+ // Combined form: operandTypeOrValue is a full "<prefix>:<expression>" string
1427
+ // like "twin:jsonPath:$.foo" rather than the canonical separation of @type +
1428
+ // @value/expression. Detect by walking the registered datasource keys for a prefix
1429
+ // match; on hit, split into key + expression so resolveDataSourceByKey can evaluate.
1430
+ if (!dataSources[sourceKey]) {
1431
+ const matchingKey = Object.keys(dataSources).find(k => sourceKey.startsWith(`${k}:`));
1432
+ if (matchingKey) {
1433
+ value = sourceKey.slice(matchingKey.length + 1);
1434
+ sourceKey = matchingKey;
1435
+ }
1436
+ }
1437
+ if (!dataSources[sourceKey] || !Is.stringValue(value)) {
1438
+ return undefined;
1439
+ }
1440
+ const resolved = this.resolveDataSourceByKey(sourceKey, value, dataSources);
1441
+ return { source: resolved.source, jsonPath: resolved.target };
1442
+ }
1443
+ /**
1444
+ * Calculate an operand value.
1445
+ * @param operand The operand.
1446
+ * @param dataSources The available prefixed operand sources.
1447
+ * @param constraint Optional constraint providing additional context for operand resolution.
1448
+ * @param isLeftOperand True when resolving the left operand of the constraint.
1449
+ * @returns The resolved operand value.
1450
+ * @throws GeneralError When a known ODRL built-in left operand cannot be resolved.
1451
+ * @internal
1452
+ */
1453
+ calculateOperandValue(operand, dataSources, constraint, isLeftOperand) {
1454
+ let jsonPath;
1455
+ let operandRoot;
1456
+ if (Is.stringValue(operand)) {
1457
+ const expression = this.extractJsonPathExpressionFromConstraint(constraint, operand);
1458
+ let resolvedOperand = operand;
1459
+ if (operand === OdrlTwinVocabulary.JsonPath &&
1460
+ Is.object(constraint)) {
1461
+ const dataSourceOverride = constraint[OdrlTwinVocabulary.JsonPathDataSource];
1462
+ if (Is.stringValue(dataSourceOverride)) {
1463
+ resolvedOperand = dataSourceOverride;
1464
+ }
1465
+ }
1466
+ const lookup = this.tryResolveOperandLookup(resolvedOperand, expression ?? operand, dataSources);
1467
+ if (lookup) {
1468
+ jsonPath = lookup.jsonPath;
1469
+ operandRoot = lookup.source;
1470
+ }
1471
+ else {
1472
+ const builtIn = this.resolveOdrlBuiltInLeftOperand(operand);
1473
+ if (!Is.undefined(builtIn)) {
1474
+ return builtIn;
1475
+ }
1476
+ if (isLeftOperand) {
1477
+ if (this.operatorSupportsInformationLookup(constraint?.operator)) {
1478
+ const infoResult = this.resolveInformationKeyOperand(operand, dataSources);
1479
+ if (infoResult.found) {
1480
+ return infoResult.value;
1481
+ }
1482
+ if (this.isKnownOdrlBuiltInOperand(operand)) {
1483
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "leftOperandNotSupported", {
1484
+ leftOperand: operand
1485
+ });
1486
+ }
1487
+ }
1488
+ else if (this.isKnownOdrlBuiltInOperand(operand)) {
1489
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "leftOperandNotSupported", {
1490
+ leftOperand: operand
1491
+ });
1492
+ }
1493
+ }
1494
+ }
1495
+ }
1496
+ else if (Is.object(operand)) {
1497
+ const typedOperand = operand;
1498
+ // Is this an object { "@value": "18", "@type": "xsd:integer" } ?
1499
+ const value = this.extractJsonPathExpressionFromTypedOperand(typedOperand) ??
1500
+ typedOperand["@value"];
1501
+ const type = typedOperand["@type"];
1502
+ if (Is.stringValue(type)) {
1503
+ let resolvedType = type;
1504
+ if (this.isTwinJsonPathOperandType(type)) {
1505
+ const dataSourceOverride = typedOperand[OdrlTwinVocabulary.JsonPathDataSource];
1506
+ if (Is.stringValue(dataSourceOverride)) {
1507
+ resolvedType = dataSourceOverride;
1508
+ }
1509
+ }
1510
+ const lookup = this.tryResolveOperandLookup(resolvedType, value, dataSources);
1511
+ if (lookup) {
1512
+ jsonPath = lookup.jsonPath;
1513
+ operandRoot = lookup.source;
1514
+ }
1515
+ else {
1516
+ const xsdValue = this.coerceXsdType(value, type);
1517
+ if (!Is.undefined(xsdValue)) {
1518
+ return xsdValue;
1519
+ }
1520
+ }
1521
+ }
1522
+ }
1523
+ // We have a JSON Path to resolve
1524
+ if (Is.stringValue(jsonPath)) {
1525
+ const jsonPaths = JsonPathHelper.query(jsonPath, operandRoot);
1526
+ if (jsonPaths.length === 0) {
1527
+ // No matches
1528
+ return undefined;
1529
+ }
1530
+ else if (jsonPaths.length === 1) {
1531
+ // Single match - return the value directly
1532
+ return jsonPaths[0].value;
1533
+ }
1534
+ // Multiple matches - return array of values
1535
+ return jsonPaths.map(p => p.value);
1536
+ }
1537
+ // Not JSON Path or object value so return as is
1538
+ return operand;
1539
+ }
1540
+ /**
1541
+ * Determine if a target object uses the canonical twin:jsonPath object format.
1542
+ * @param target The target value.
1543
+ * @returns True if the target is a canonical twin:jsonPath object.
1544
+ * @internal
1545
+ */
1546
+ isTwinJsonPathTarget(target) {
1547
+ return Is.object(target) && this.isTwinJsonPathOperandType(OdrlPolicyHelper.getType(target));
1548
+ }
1549
+ /**
1550
+ * Determine if the operand type is a jsonPath namespace.
1551
+ * @param type The operand type.
1552
+ * @returns True if the type is twin:jsonPath.
1553
+ * @internal
1554
+ */
1555
+ isTwinJsonPathOperandType(type) {
1556
+ return type === OdrlTwinVocabulary.JsonPath;
1557
+ }
1558
+ /**
1559
+ * Normalize canonical jsonPath alias to the legacy jsonpath namespace key.
1560
+ * @param operandTypeOrValue The raw operand type or value.
1561
+ * @returns The normalized operand type/value.
1562
+ * @internal
1563
+ */
1564
+ normalizeTwinJsonPathOperandAlias(operandTypeOrValue) {
1565
+ if (operandTypeOrValue === OdrlTwinVocabulary.JsonPath ||
1566
+ operandTypeOrValue.startsWith(`${OdrlTwinVocabulary.JsonPath}:`)) {
1567
+ const suffix = operandTypeOrValue.slice(OdrlTwinVocabulary.JsonPath.length);
1568
+ return `${OdrlTwinVocabulary.DataSourceKey}${suffix}`;
1569
+ }
1570
+ return operandTypeOrValue;
1571
+ }
1572
+ /**
1573
+ * Extract canonical jsonPath expression from typed operand object.
1574
+ * @param operand The typed operand.
1575
+ * @returns The jsonPath expression if present.
1576
+ * @throws GeneralError if the operand type is twin:jsonPath but no expression is provided.
1577
+ * @internal
1578
+ */
1579
+ extractJsonPathExpressionFromTypedOperand(operand) {
1580
+ if (!this.isTwinJsonPathOperandType(operand["@type"])) {
1581
+ return undefined;
1582
+ }
1583
+ const expression = operand[OdrlTwinVocabulary.JsonPathExpression];
1584
+ if (Is.stringValue(expression)) {
1585
+ return expression;
1586
+ }
1587
+ const type = operand["@type"];
1588
+ if (type === OdrlTwinVocabulary.JsonPath) {
1589
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
1590
+ operand: "rightOperand"
1591
+ });
1592
+ }
1593
+ }
1594
+ /**
1595
+ * Extract canonical jsonPath expression from constraint for leftOperand aliases.
1596
+ * @param constraint The constraint containing the left operand.
1597
+ * @param leftOperand The left operand.
1598
+ * @returns The expression if canonical form is used.
1599
+ * @throws GeneralError if leftOperand uses a twin:jsonPath prefix without an expression.
1600
+ * @internal
1601
+ */
1602
+ extractJsonPathExpressionFromConstraint(constraint, leftOperand) {
1603
+ if (!Is.object(constraint)) {
1604
+ return undefined;
1605
+ }
1606
+ if (constraint.leftOperand !== leftOperand) {
1607
+ return undefined;
1608
+ }
1609
+ if (leftOperand.startsWith(`${OdrlTwinVocabulary.JsonPath}:`)) {
1610
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
1611
+ operand: "leftOperand"
1612
+ });
1613
+ }
1614
+ if (leftOperand !== OdrlTwinVocabulary.JsonPath) {
1615
+ return undefined;
1616
+ }
1617
+ const expression = constraint[OdrlTwinVocabulary.JsonPathExpression];
1618
+ if (Is.stringValue(expression)) {
1619
+ return expression;
1620
+ }
1621
+ throw new GeneralError(DefaultPolicyArbiter.CLASS_NAME, "jsonPathExpressionMissing", {
1622
+ operand: "leftOperand"
1623
+ });
1624
+ }
1625
+ /**
1626
+ * Evaluate an ODRL operator against resolved operands.
1627
+ * @param operator The operator.
1628
+ * @param left The resolved left operand.
1629
+ * @param right The resolved right operand.
1630
+ * @returns True if the comparison is satisfied.
1631
+ * @internal
1632
+ */
1633
+ evaluateOperator(operator, left, right) {
1634
+ // Handle array/collection left values (e.g. JSONPath returning multiple matches).
1635
+ const leftValues = ArrayHelper.fromObjectOrArray(left ?? []);
1636
+ switch (operator) {
1637
+ case OdrlOperatorType.Eq:
1638
+ return leftValues.some(v => ObjectHelper.equal(v, right, false) || this.compareDurationEq(v, right));
1639
+ case OdrlOperatorType.Neq:
1640
+ return leftValues.every(v => !ObjectHelper.equal(v, right, false) && !this.compareDurationEq(v, right));
1641
+ case OdrlOperatorType.Gt:
1642
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a > b));
1643
+ case OdrlOperatorType.Gteq:
1644
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a >= b));
1645
+ case OdrlOperatorType.Lt:
1646
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a < b));
1647
+ case OdrlOperatorType.Lteq:
1648
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a <= b));
1649
+ case OdrlOperatorType.IsAnyOf: {
1650
+ return leftValues.some(v => {
1651
+ const stringValue = Is.string(v) || Is.number(v) || Is.boolean(v) ? String(v) : JSON.stringify(v);
1652
+ return (ArrayHelper.fromObjectOrArray(right) ?? []).includes(stringValue);
1653
+ });
1654
+ }
1655
+ case OdrlOperatorType.IsAllOf: {
1656
+ return ObjectHelper.equal(leftValues, ArrayHelper.fromObjectOrArray(right) ?? [], false);
1657
+ }
1658
+ case OdrlOperatorType.IsNoneOf: {
1659
+ return leftValues.every(v => {
1660
+ const stringValue = Is.string(v) || Is.number(v) || Is.boolean(v) ? String(v) : JSON.stringify(v);
1661
+ return !(ArrayHelper.fromObjectOrArray(right) ?? []).includes(stringValue);
1662
+ });
1663
+ }
1664
+ case OdrlOperatorType.LocTimeEq:
1665
+ return leftValues.some(v => ObjectHelper.equal(v, right, false));
1666
+ case OdrlOperatorType.LocTimeGteq:
1667
+ return leftValues.some(v => this.compareOrdered(v, right, (a, b) => a >= b));
1668
+ case OdrlOperatorType.IsA:
1669
+ case OdrlOperatorType.HasPart:
1670
+ case OdrlOperatorType.IsPartOf:
1671
+ // For now, treat these as simple equality/ordering semantics where meaningful.
1672
+ // Profiles can introduce richer semantics via additional arbiters.
1673
+ return leftValues.some(v => ObjectHelper.equal(v, right, false));
1674
+ default:
1675
+ return false;
1676
+ }
1677
+ }
1678
+ /**
1679
+ * Compare values with numeric/date/string coercion.
1680
+ * @param left The left value.
1681
+ * @param right The right value.
1682
+ * @param compare Comparison operator.
1683
+ * @returns True if ordered comparison passes.
1684
+ * @internal
1685
+ */
1686
+ compareOrdered(left, right, compare) {
1687
+ // A genuine Date instance (e.g. the built-in "dateTime" left operand, which resolves to
1688
+ // new Date()) must always be compared as a date. Falling through to the generic numeric
1689
+ // branch below is unsafe here: Coerce.number(dateInstance) returns its epoch millisecond
1690
+ // timestamp, while Coerce.number(isoDateString) uses Number.parseFloat, which silently
1691
+ // truncates at the first "-" and returns just the leading year (e.g. "2035-12-31..." -> 2035)
1692
+ // - comparing an epoch millisecond timestamp against a bare 4-digit year is never meaningful.
1693
+ if (Is.date(left) || Is.date(right)) {
1694
+ const dateLeft = Coerce.dateTime(left);
1695
+ const dateRight = Coerce.dateTime(right);
1696
+ // A Date on one side means this can only be a date comparison - if the other side fails to
1697
+ // coerce (e.g. a malformed date string), fail closed rather than falling through to the
1698
+ // numeric branch below, which would reproduce the exact epoch-ms-vs-truncated-number bug
1699
+ // this whole guard exists to prevent.
1700
+ if (Is.undefined(dateLeft) || Is.undefined(dateRight)) {
1701
+ return false;
1702
+ }
1703
+ return compare(dateLeft.getTime(), dateRight.getTime());
1704
+ }
1705
+ // ISO date/datetime strings must be compared as dates; Coerce.number() silently
1706
+ // truncates at the first "-" so same-year dates (e.g. "2025-06-01" vs "2025-12-31")
1707
+ // both become 2025 and compare equal, masking the real chronological difference.
1708
+ if (Is.dateTimeString(left) ||
1709
+ Is.dateString(left) ||
1710
+ Is.dateTimeString(right) ||
1711
+ Is.dateString(right)) {
1712
+ const dateLeft = Coerce.dateTime(left);
1713
+ const dateRight = Coerce.dateTime(right);
1714
+ if (Is.undefined(dateLeft) || Is.undefined(dateRight)) {
1715
+ return false;
1716
+ }
1717
+ return compare(dateLeft.getTime(), dateRight.getTime());
1718
+ }
1719
+ // Time strings have the same truncation problem: Coerce.number("09:30:00") = 9,
1720
+ // so same-hour times compare equal. Coerce.time normalises both sides to a
1721
+ // 1970-01-01 base date for a meaningful time-of-day comparison.
1722
+ if (Is.timeString(left) || Is.timeString(right)) {
1723
+ const timeLeft = Coerce.time(left);
1724
+ const timeRight = Coerce.time(right);
1725
+ if (Is.undefined(timeLeft) || Is.undefined(timeRight)) {
1726
+ return false;
1727
+ }
1728
+ return compare(timeLeft.getTime(), timeRight.getTime());
1729
+ }
1730
+ const leftNum = Coerce.number(left);
1731
+ const rightNum = Coerce.number(right);
1732
+ if (!Is.undefined(leftNum) && !Is.undefined(rightNum)) {
1733
+ return compare(leftNum, rightNum);
1734
+ }
1735
+ const leftDate = Coerce.dateTime(left);
1736
+ const rightDate = Coerce.dateTime(right);
1737
+ if (!Is.undefined(leftDate) && !Is.undefined(rightDate)) {
1738
+ return compare(leftDate.getTime(), rightDate.getTime());
1739
+ }
1740
+ const leftDuration = Coerce.duration(left);
1741
+ const rightDuration = Coerce.duration(right);
1742
+ if (!Is.undefined(leftDuration) && !Is.undefined(rightDuration)) {
1743
+ return compare(Duration.toSeconds(leftDuration), Duration.toSeconds(rightDuration));
1744
+ }
1745
+ // Only use string ordering when both operands are actual strings.
1746
+ // Avoid coercing other types into strings, as that can cause
1747
+ // unintended comparisons like 18 >= "$.minAge" evaluating to true.
1748
+ if (Is.string(left) && Is.string(right)) {
1749
+ return compare(left.localeCompare(right), 0);
1750
+ }
1751
+ return false;
1752
+ }
1753
+ /**
1754
+ * Test whether two values are equal when interpreted as ISO 8601 durations.
1755
+ * Returns false if either side cannot be coerced to a duration.
1756
+ * @param left The left value.
1757
+ * @param right The right value.
1758
+ * @returns True when both sides represent the same duration in seconds.
1759
+ * @internal
1760
+ */
1761
+ compareDurationEq(left, right) {
1762
+ const leftDuration = Coerce.duration(left);
1763
+ const rightDuration = Coerce.duration(right);
1764
+ if (!Is.undefined(leftDuration) && !Is.undefined(rightDuration)) {
1765
+ return Duration.toSeconds(leftDuration) === Duration.toSeconds(rightDuration);
1766
+ }
1767
+ return false;
1768
+ }
1769
+ /**
1770
+ * Resolve an ODRL built-in left operand to its runtime value.
1771
+ * Returns the resolved value for natively-handled operands and undefined for all others.
1772
+ * Callers are responsible for throwing when an unresolved known built-in is unacceptable.
1773
+ * @param operand The left operand string.
1774
+ * @returns The resolved value, or undefined when the operand has no native resolution.
1775
+ * @internal
1776
+ */
1777
+ resolveOdrlBuiltInLeftOperand(operand) {
1778
+ if (operand === OdrlLeftOperandType.DateTime) {
1779
+ return new Date();
1780
+ }
1781
+ return undefined;
1782
+ }
1783
+ /**
1784
+ * Determine whether the operand string is a recognised ODRL built-in left operand.
1785
+ * @param operand The operand string to test.
1786
+ * @returns True when the operand appears in the OdrlLeftOperandType vocabulary.
1787
+ * @internal
1788
+ */
1789
+ isKnownOdrlBuiltInOperand(operand) {
1790
+ return Object.values(OdrlLeftOperandType).includes(operand);
1791
+ }
1792
+ /**
1793
+ * Determine whether the operator supports resolving left operands from the information object.
1794
+ * Value-comparison and set operators are supported; structural/relationship operators are not.
1795
+ * @param operator The operator to test.
1796
+ * @returns True when information-key lookup is appropriate for this operator.
1797
+ * @internal
1798
+ */
1799
+ operatorSupportsInformationLookup(operator) {
1800
+ switch (operator) {
1801
+ case OdrlOperatorType.Eq:
1802
+ case OdrlOperatorType.Neq:
1803
+ case OdrlOperatorType.Gt:
1804
+ case OdrlOperatorType.Gteq:
1805
+ case OdrlOperatorType.Lt:
1806
+ case OdrlOperatorType.Lteq:
1807
+ case OdrlOperatorType.IsAnyOf:
1808
+ case OdrlOperatorType.IsAllOf:
1809
+ case OdrlOperatorType.IsNoneOf:
1810
+ case OdrlOperatorType.LocTimeEq:
1811
+ case OdrlOperatorType.LocTimeGteq:
1812
+ return true;
1813
+ default:
1814
+ return false;
1815
+ }
1816
+ }
1817
+ /**
1818
+ * Attempt to resolve a left operand string as a key in the information object.
1819
+ * @param operand The left operand string.
1820
+ * @param dataSources The available data sources.
1821
+ * @returns An object indicating whether the key was found and its value when present.
1822
+ * @internal
1823
+ */
1824
+ resolveInformationKeyOperand(operand, dataSources) {
1825
+ const information = dataSources[OdrlTwinVocabulary.InformationSourceKey];
1826
+ if (Is.object(information) && !Is.empty(information[operand])) {
1827
+ return { found: true, value: information[operand] };
1828
+ }
1829
+ return { found: false };
1830
+ }
1831
+ /**
1832
+ * Coerce a value to a specific XSD type.
1833
+ * @param value The value to coerce.
1834
+ * @param type The XSD type.
1835
+ * @returns The coerced value, or undefined when coercion is not possible.
1836
+ * @internal
1837
+ */
1838
+ coerceXsdType(value, type) {
1839
+ if ([
1840
+ "xsd:string",
1841
+ "xsd:normalizedString",
1842
+ "xsd:token",
1843
+ "xsd:anyURI",
1844
+ "xsd:QName",
1845
+ "xsd:NOTATION"
1846
+ ].includes(type)) {
1847
+ // Handle standard xsd types
1848
+ return Coerce.string(value);
1849
+ }
1850
+ else if ([
1851
+ "xsd:integer",
1852
+ "xsd:decimal",
1853
+ "xsd:float",
1854
+ "xsd:double",
1855
+ "xsd:long",
1856
+ "xsd:int",
1857
+ "xsd:short",
1858
+ "xsd:byte"
1859
+ ].includes(type)) {
1860
+ // Handle standard xsd types
1861
+ return Coerce.number(value);
1862
+ }
1863
+ else if (type === "xsd:boolean") {
1864
+ // Handle standard xsd types
1865
+ return Coerce.boolean(value);
1866
+ }
1867
+ else if (["xsd:date", "xsd:dateTime", "xsd:time"].includes(type)) {
1868
+ // Handle standard xsd types
1869
+ return Coerce.dateTime(value);
1870
+ }
1871
+ else if (type === "xsd:duration") {
1872
+ // Handle standard xsd types - convert to total seconds for ordered comparison.
1873
+ // Falls back to numeric coercion for non-ISO values (e.g. bare seconds as a number).
1874
+ const dur = Coerce.duration(value);
1875
+ return Is.undefined(dur) ? Coerce.number(value) : Duration.toSeconds(dur);
1876
+ }
1877
+ return undefined;
1878
+ }
1879
+ }
1880
+ //# sourceMappingURL=defaultPolicyArbiter.js.map