@cullet/erp-core 1.5.0 → 2.0.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 (306) hide show
  1. package/KIT_CONTEXT.md +2 -2
  2. package/dist/abac/index.d.cts +2 -2
  3. package/dist/abac/index.d.ts +2 -2
  4. package/dist/aggregate-version.cjs +14 -0
  5. package/dist/aggregate-version.cjs.map +1 -0
  6. package/dist/aggregate-version.js +9 -0
  7. package/dist/aggregate-version.js.map +1 -0
  8. package/dist/app-error.cjs +8 -1
  9. package/dist/app-error.cjs.map +1 -1
  10. package/dist/app-error.js +8 -1
  11. package/dist/app-error.js.map +1 -1
  12. package/dist/application/index.cjs +8 -4
  13. package/dist/application/index.d.cts +2 -2
  14. package/dist/application/index.d.ts +2 -2
  15. package/dist/application/index.js +1 -2
  16. package/dist/authorization-error.cjs +67 -17
  17. package/dist/authorization-error.cjs.map +1 -1
  18. package/dist/authorization-error.d.cts +6 -2
  19. package/dist/authorization-error.d.ts +6 -2
  20. package/dist/authorization-error.js +50 -18
  21. package/dist/authorization-error.js.map +1 -1
  22. package/dist/authorizer.port.d.cts +18 -3
  23. package/dist/authorizer.port.d.ts +18 -3
  24. package/dist/composite-authorizer.d.cts +63 -10
  25. package/dist/composite-authorizer.d.ts +63 -10
  26. package/dist/condition-evaluator.cjs +115 -184
  27. package/dist/condition-evaluator.cjs.map +1 -1
  28. package/dist/condition-evaluator.js +116 -179
  29. package/dist/condition-evaluator.js.map +1 -1
  30. package/dist/core-config.d.cts +53 -6
  31. package/dist/core-config.d.ts +53 -6
  32. package/dist/decorate.cjs +14 -0
  33. package/dist/decorate.js +9 -0
  34. package/dist/domain/index.cjs +107 -2
  35. package/dist/domain/index.cjs.map +1 -0
  36. package/dist/domain/index.d.cts +25 -1
  37. package/dist/domain/index.d.ts +25 -1
  38. package/dist/domain/index.js +99 -3
  39. package/dist/domain/index.js.map +1 -0
  40. package/dist/domain-event-contracts.cjs +12 -8
  41. package/dist/domain-event-contracts.cjs.map +1 -1
  42. package/dist/domain-event-contracts.d.cts +29 -4
  43. package/dist/domain-event-contracts.d.ts +29 -4
  44. package/dist/domain-event-contracts.js +11 -7
  45. package/dist/domain-event-contracts.js.map +1 -1
  46. package/dist/domain-exception.cjs +2 -1
  47. package/dist/domain-exception.cjs.map +1 -1
  48. package/dist/domain-exception.js +2 -1
  49. package/dist/domain-exception.js.map +1 -1
  50. package/dist/errors/index.cjs +1 -1
  51. package/dist/errors/index.d.cts +1 -1
  52. package/dist/errors/index.d.ts +1 -1
  53. package/dist/errors/index.js +2 -2
  54. package/dist/exceptions/index.cjs +2 -2
  55. package/dist/exceptions/index.d.cts +1 -2
  56. package/dist/exceptions/index.d.ts +1 -2
  57. package/dist/exceptions/index.js +2 -2
  58. package/dist/gate-engine-registry.cjs.map +1 -1
  59. package/dist/gate-engine-registry.d.cts +1 -1
  60. package/dist/gate-engine-registry.d.ts +1 -1
  61. package/dist/gate-engine-registry.js.map +1 -1
  62. package/dist/gate-v1-payload.schema.cjs +11 -6
  63. package/dist/gate-v1-payload.schema.cjs.map +1 -1
  64. package/dist/gate-v1-payload.schema.js +11 -6
  65. package/dist/gate-v1-payload.schema.js.map +1 -1
  66. package/dist/hashing.cjs +1 -1
  67. package/dist/hashing.cjs.map +1 -1
  68. package/dist/hashing.js +2 -2
  69. package/dist/hashing.js.map +1 -1
  70. package/dist/immutable.cjs +25 -12
  71. package/dist/immutable.cjs.map +1 -1
  72. package/dist/immutable.js +24 -11
  73. package/dist/immutable.js.map +1 -1
  74. package/dist/index.cjs +13 -10
  75. package/dist/index.cjs.map +1 -1
  76. package/dist/index.d.cts +9 -10
  77. package/dist/index.d.ts +9 -10
  78. package/dist/index.js +7 -9
  79. package/dist/index.js.map +1 -1
  80. package/dist/invalid-state-transition-exception.cjs +6 -6
  81. package/dist/invalid-state-transition-exception.cjs.map +1 -1
  82. package/dist/invalid-state-transition-exception.js +6 -6
  83. package/dist/invalid-state-transition-exception.js.map +1 -1
  84. package/dist/invariant-violation-exception.cjs +2 -2
  85. package/dist/invariant-violation-exception.cjs.map +1 -1
  86. package/dist/invariant-violation-exception.js +2 -2
  87. package/dist/invariant-violation-exception.js.map +1 -1
  88. package/dist/not-found-error.cjs +6 -4
  89. package/dist/not-found-error.cjs.map +1 -1
  90. package/dist/not-found-error.js +6 -4
  91. package/dist/not-found-error.js.map +1 -1
  92. package/dist/outcome.cjs +5 -5
  93. package/dist/outcome.cjs.map +1 -1
  94. package/dist/outcome.js +5 -5
  95. package/dist/outcome.js.map +1 -1
  96. package/dist/parse-gate-payload.d.cts +1 -1
  97. package/dist/parse-gate-payload.d.ts +1 -1
  98. package/dist/path.d.cts +2 -2
  99. package/dist/path.d.ts +2 -2
  100. package/dist/plugin.cjs +23 -13
  101. package/dist/plugin.cjs.map +1 -1
  102. package/dist/plugin.d.cts +22 -8
  103. package/dist/plugin.d.ts +22 -8
  104. package/dist/plugin.js +23 -13
  105. package/dist/plugin.js.map +1 -1
  106. package/dist/policies/engines/index.d.cts +1 -1
  107. package/dist/policies/engines/index.d.ts +1 -1
  108. package/dist/policies/engines/v1/gate/index.d.cts +3 -15
  109. package/dist/policies/engines/v1/gate/index.d.ts +3 -15
  110. package/dist/policies/index.d.cts +1 -1
  111. package/dist/policies/index.d.ts +1 -1
  112. package/dist/policy-bridge.cjs +30 -2
  113. package/dist/policy-bridge.cjs.map +1 -1
  114. package/dist/policy-bridge.d.cts +18 -0
  115. package/dist/policy-bridge.d.ts +18 -0
  116. package/dist/policy-bridge.js +30 -2
  117. package/dist/policy-bridge.js.map +1 -1
  118. package/dist/policy-service.cjs +40 -46
  119. package/dist/policy-service.cjs.map +1 -1
  120. package/dist/policy-service.d.cts +72 -9
  121. package/dist/policy-service.d.ts +72 -9
  122. package/dist/policy-service.js +42 -48
  123. package/dist/policy-service.js.map +1 -1
  124. package/dist/requested-by.cjs +1 -1
  125. package/dist/requested-by.cjs.map +1 -1
  126. package/dist/requested-by.js +1 -1
  127. package/dist/requested-by.js.map +1 -1
  128. package/dist/result.cjs +29 -1
  129. package/dist/result.cjs.map +1 -1
  130. package/dist/result.d.cts +12 -0
  131. package/dist/result.d.ts +12 -0
  132. package/dist/result.js +29 -1
  133. package/dist/result.js.map +1 -1
  134. package/dist/rule.cjs +94 -24
  135. package/dist/rule.cjs.map +1 -1
  136. package/dist/rule.js +94 -24
  137. package/dist/rule.js.map +1 -1
  138. package/dist/ruleset-registry.cjs +19 -0
  139. package/dist/ruleset-registry.cjs.map +1 -1
  140. package/dist/ruleset-registry.js +19 -0
  141. package/dist/ruleset-registry.js.map +1 -1
  142. package/dist/stable-stringify.cjs +43 -3
  143. package/dist/stable-stringify.cjs.map +1 -1
  144. package/dist/stable-stringify.js +38 -4
  145. package/dist/stable-stringify.js.map +1 -1
  146. package/dist/temporal-snapshot.d.cts +87 -0
  147. package/dist/temporal-snapshot.d.ts +87 -0
  148. package/dist/temporal-use-case.cjs +169 -16
  149. package/dist/temporal-use-case.cjs.map +1 -1
  150. package/dist/temporal-use-case.d.cts +76 -43
  151. package/dist/temporal-use-case.d.ts +76 -43
  152. package/dist/temporal-use-case.js +162 -15
  153. package/dist/temporal-use-case.js.map +1 -1
  154. package/dist/unexpected-error.cjs +4 -2
  155. package/dist/unexpected-error.cjs.map +1 -1
  156. package/dist/unexpected-error.js +4 -2
  157. package/dist/unexpected-error.js.map +1 -1
  158. package/dist/uuid-identifier.cjs +140 -2
  159. package/dist/uuid-identifier.cjs.map +1 -1
  160. package/dist/uuid-identifier.d.cts +26 -7
  161. package/dist/uuid-identifier.d.ts +26 -7
  162. package/dist/uuid-identifier.js +135 -3
  163. package/dist/uuid-identifier.js.map +1 -1
  164. package/dist/uuid.cjs +11 -6
  165. package/dist/uuid.cjs.map +1 -1
  166. package/dist/uuid.js +11 -6
  167. package/dist/uuid.js.map +1 -1
  168. package/dist/validation-code.cjs +9 -0
  169. package/dist/validation-code.cjs.map +1 -1
  170. package/dist/validation-code.d.cts +3 -0
  171. package/dist/validation-code.d.ts +3 -0
  172. package/dist/validation-code.js +9 -0
  173. package/dist/validation-code.js.map +1 -1
  174. package/dist/validation-error.cjs +42 -67
  175. package/dist/validation-error.cjs.map +1 -1
  176. package/dist/validation-error.d.cts +24 -8
  177. package/dist/validation-error.d.ts +24 -8
  178. package/dist/validation-error.js +41 -66
  179. package/dist/validation-error.js.map +1 -1
  180. package/dist/validation-exception.cjs +17 -6
  181. package/dist/validation-exception.cjs.map +1 -1
  182. package/dist/validation-exception.d.cts +33 -9
  183. package/dist/validation-exception.d.ts +33 -9
  184. package/dist/validation-exception.js +17 -6
  185. package/dist/validation-exception.js.map +1 -1
  186. package/dist/validation-field.cjs +3 -0
  187. package/dist/validation-field.cjs.map +1 -1
  188. package/dist/validation-field.d.cts +1 -0
  189. package/dist/validation-field.d.ts +1 -0
  190. package/dist/validation-field.js +3 -0
  191. package/dist/validation-field.js.map +1 -1
  192. package/dist/value-object-ruleset.contracts.d.cts +10 -0
  193. package/dist/value-object-ruleset.contracts.d.ts +10 -0
  194. package/dist/value-object.cjs +12 -2
  195. package/dist/value-object.cjs.map +1 -1
  196. package/dist/value-object.d.cts +16 -0
  197. package/dist/value-object.d.ts +16 -0
  198. package/dist/value-object.js +13 -3
  199. package/dist/value-object.js.map +1 -1
  200. package/dist/version.d.cts +27 -0
  201. package/dist/version.d.ts +27 -0
  202. package/dist/versioning/index.d.cts +2 -2
  203. package/dist/versioning/index.d.ts +2 -2
  204. package/meta.json +3 -2
  205. package/package.json +1 -1
  206. package/src/core/abac/authorizer.ts +60 -10
  207. package/src/core/abac/domain/policy-set.ts +52 -5
  208. package/src/core/abac/domain/rule.ts +28 -16
  209. package/src/core/abac/index.ts +7 -1
  210. package/src/core/application/commands/command.ts +14 -1
  211. package/src/core/application/commands/requested-by.ts +12 -3
  212. package/src/core/application/index.ts +2 -1
  213. package/src/core/application/policy-error-mapper.ts +6 -0
  214. package/src/core/application/ports/index.ts +7 -0
  215. package/src/core/application/ports/temporal-repository.port.ts +35 -2
  216. package/src/core/application/queries/index.ts +1 -1
  217. package/src/core/application/queries/query.ts +19 -2
  218. package/src/core/application/temporal/temporal-use-case.ts +31 -4
  219. package/src/core/application/use-case.ts +44 -7
  220. package/src/core/config/core-config.ts +46 -25
  221. package/src/core/config/index.ts +1 -0
  222. package/src/core/config/policy-reporter.ts +32 -1
  223. package/src/core/domain/entity.ts +50 -7
  224. package/src/core/domain/rulesets/entity-ruleset.contracts.ts +0 -2
  225. package/src/core/domain/rulesets/ruleset-registry.ts +24 -0
  226. package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +1 -7
  227. package/src/core/domain/temporal/half-open-interval.ts +34 -0
  228. package/src/core/domain/temporal/temporal-snapshot.ts +13 -2
  229. package/src/core/domain/temporal/transaction-time.ts +19 -15
  230. package/src/core/domain/temporal/valid-time.ts +20 -15
  231. package/src/core/domain/uuid-identifier.ts +5 -3
  232. package/src/core/domain/value-object.ts +17 -0
  233. package/src/core/errors/authentication-error.ts +5 -31
  234. package/src/core/errors/authorization-error.ts +12 -20
  235. package/src/core/errors/business-rule-violation-error.ts +4 -1
  236. package/src/core/errors/idempotency-error.ts +5 -2
  237. package/src/core/errors/index.ts +4 -0
  238. package/src/core/errors/integration-error.ts +4 -24
  239. package/src/core/errors/legacy-incompatible-error.ts +1 -0
  240. package/src/core/errors/not-found-error.ts +4 -1
  241. package/src/core/errors/temporal-error.ts +22 -20
  242. package/src/core/errors/unexpected-error.ts +5 -1
  243. package/src/core/errors/utils/factory-helpers.ts +70 -0
  244. package/src/core/errors/utils/index.ts +5 -0
  245. package/src/core/errors/utils/json-safe.ts +15 -1
  246. package/src/core/errors/validation-error.ts +4 -1
  247. package/src/core/exceptions/business-rule-violation-exception.ts +2 -1
  248. package/src/core/exceptions/domain-exception.ts +9 -1
  249. package/src/core/exceptions/entity-not-found-exception.ts +5 -1
  250. package/src/core/exceptions/invalid-state-transition-exception.ts +5 -2
  251. package/src/core/exceptions/invariant-violation-exception.ts +2 -2
  252. package/src/core/exceptions/validation-code.ts +14 -0
  253. package/src/core/exceptions/validation-exception.ts +44 -5
  254. package/src/core/exceptions/validation-field.ts +11 -1
  255. package/src/core/plugins/plugin.ts +25 -15
  256. package/src/core/plugins/types.ts +4 -2
  257. package/src/core/policies/asof/asof.ts +12 -0
  258. package/src/core/policies/catalog/policy-catalog-entry.ts +3 -1
  259. package/src/core/policies/catalog/policy-catalog.ts +5 -1
  260. package/src/core/policies/context/context-builder.ts +20 -0
  261. package/src/core/policies/context/context-resolver.ts +5 -0
  262. package/src/core/policies/context/context-seed.ts +11 -0
  263. package/src/core/policies/defs/in-memory-policy-definition-repo.ts +0 -2
  264. package/src/core/policies/engines/parse-gate-payload.ts +9 -0
  265. package/src/core/policies/engines/v1/condition-date-operands.ts +112 -0
  266. package/src/core/policies/engines/v1/condition-evaluator.ts +99 -325
  267. package/src/core/policies/engines/v1/condition-schema.ts +23 -19
  268. package/src/core/policies/engines/v1/condition-types.ts +18 -11
  269. package/src/core/policies/index.ts +4 -6
  270. package/src/core/policies/service/policy-service.ts +46 -35
  271. package/src/core/policies/utils/hash.ts +5 -69
  272. package/src/core/policies/utils/result.ts +1 -1
  273. package/src/core/rbac/access-request.ts +12 -2
  274. package/src/core/rbac/authorizer.ts +8 -1
  275. package/src/core/rbac/domain/role.ts +20 -0
  276. package/src/core/rbac/domain/scope.ts +6 -1
  277. package/src/core/result/index.ts +5 -0
  278. package/src/core/result/outcome.ts +5 -7
  279. package/src/core/result/result.ts +34 -1
  280. package/src/core/shared/hashing.ts +6 -3
  281. package/src/core/shared/immutable.ts +22 -4
  282. package/src/core/shared/stable-stringify.ts +91 -4
  283. package/src/core/shared/uuid.ts +11 -6
  284. package/src/core/versioning/domain-event-contracts.ts +43 -15
  285. package/src/core/versioning/index.ts +1 -0
  286. package/src/core/versioning/version.ts +30 -1
  287. package/src/domain/index.ts +5 -0
  288. package/src/examples/rulesets/entity/order-creation-rules-v1.ts +1 -1
  289. package/src/examples/rulesets/entity/order-invariants-v1.ts +2 -4
  290. package/src/examples/rulesets/entity/order-invariants-v2.ts +2 -4
  291. package/src/examples/rulesets/value-object/cpf-rules-v1.ts +2 -4
  292. package/src/examples/rulesets/value-object/cpf-rules-v2.ts +2 -4
  293. package/src/examples/rulesets/value-object/person-name-rules-v1.ts +2 -4
  294. package/src/examples/rulesets/value-object/person-name-rules-v2.ts +2 -4
  295. package/src/result/index.ts +7 -2
  296. package/src/version.ts +1 -1
  297. package/dist/domain-exception.d.cts +0 -7
  298. package/dist/domain-exception.d.ts +0 -7
  299. package/dist/entity.cjs +0 -122
  300. package/dist/entity.cjs.map +0 -1
  301. package/dist/entity.js +0 -111
  302. package/dist/entity.js.map +0 -1
  303. package/dist/use-case.cjs +0 -92
  304. package/dist/use-case.cjs.map +0 -1
  305. package/dist/use-case.js +0 -87
  306. package/dist/use-case.js.map +0 -1
@@ -1,10 +1,14 @@
1
1
  import { PolicyContextPath } from "../../context/index.js";
2
- // Import the date util directly (not the utils barrel): the barrel re-exports
3
- // PolicyHashing, whose node:crypto import would needlessly drag a Node-only
4
- // API into the zod-free ./abac subpath.
5
- import { PolicyDateUtils } from "../../utils/date.js";
6
2
  import { Result } from "../../../result/result.js";
7
3
 
4
+ import {
5
+ CANONICAL_UTC_DATE_FORMAT,
6
+ containsInvalidDate,
7
+ evaluateRelationalNumbers,
8
+ normalizeDateOperand,
9
+ parseComparableDate,
10
+ type RelationalOperator,
11
+ } from "./condition-date-operands.js";
8
12
  import type {
9
13
  ConditionAndNode,
10
14
  ConditionLeafNode,
@@ -28,8 +32,6 @@ const INVALID_NUMERIC_OPERAND_TAG = "INVALID_NUMERIC_OPERAND";
28
32
  const INVALID_SET_OPERAND_TAG = "INVALID_SET_OPERAND";
29
33
  const EMPTY_OR_CONDITION_TAG = "EMPTY_OR_CONDITION";
30
34
  const EMPTY_AND_CONDITION_TAG = "EMPTY_AND_CONDITION";
31
- const ISO_8601_UTC_DATE_PATTERN =
32
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
33
35
 
34
36
  export interface ConditionEvaluationTrace {
35
37
  readonly conditionPath: string;
@@ -44,8 +46,6 @@ export interface TracedConditionEvaluation {
44
46
  readonly trace: ConditionEvaluationTrace;
45
47
  }
46
48
 
47
- type RelationalOperator = Extract<ConditionOp, "gt" | "gte" | "lt" | "lte">;
48
-
49
49
  export class ConditionEvaluatorV1 {
50
50
  constructor(
51
51
  private readonly context: PolicyContext,
@@ -79,106 +79,14 @@ export class ConditionEvaluatorV1 {
79
79
  message: string;
80
80
  } {
81
81
  if (error instanceof Error) {
82
- return {
83
- name: error.name,
84
- message: error.message,
85
- };
86
- }
87
-
88
- return {
89
- name: "NonErrorThrown",
90
- message: String(error),
91
- };
92
- }
93
-
94
- private static buildNullishNumericOperandMessage(
95
- node: ConditionLeafNode,
96
- actual: null | undefined,
97
- ): string {
98
- const resolvedValue = actual === null ? "null" : "undefined";
99
-
100
- return `${NULLISH_NUMERIC_OPERAND_NOT_ALLOWED_TAG}: "${node.field}" resolved to ${resolvedValue} for numeric operator "${node.op}"`;
101
- }
102
-
103
- private static buildNullishDateOperandMessage(
104
- node: ConditionLeafNode,
105
- actual: null | undefined,
106
- ): string {
107
- const resolvedValue = actual === null ? "null" : "undefined";
108
-
109
- return `${NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG}: "${node.field}" resolved to ${resolvedValue} for date comparison operator "${node.op}"`;
110
- }
111
-
112
- private static buildInvalidDateOperandMessage(
113
- node: ConditionLeafNode,
114
- ): string {
115
- return `${INVALID_DATE_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires Date or ISO 8601 UTC string operands`;
116
- }
117
-
118
- private static buildInvalidNumericOperandMessage(
119
- node: ConditionLeafNode,
120
- ): string {
121
- return `${INVALID_NUMERIC_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires numeric operands`;
122
- }
123
-
124
- private static buildInvalidSetOperandMessage(
125
- node: ConditionLeafNode,
126
- ): string {
127
- return `${INVALID_SET_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires an array operand`;
128
- }
129
-
130
- private static buildEmptyOrConditionMessage(): string {
131
- return `${EMPTY_OR_CONDITION_TAG}: OR nodes must contain at least one child condition`;
132
- }
133
-
134
- private static buildEmptyAndConditionMessage(): string {
135
- return `${EMPTY_AND_CONDITION_TAG}: AND nodes must contain at least one child condition`;
136
- }
137
-
138
- private static parseComparableDate(
139
- value: unknown,
140
- ): Result<Date, string> | null {
141
- if (value instanceof Date) {
142
- if (!PolicyDateUtils.isValid(value)) {
143
- return Result.err("Invalid Date instance");
144
- }
145
-
146
- return Result.ok(value);
147
- }
148
-
149
- if (
150
- typeof value !== "string" ||
151
- !ISO_8601_UTC_DATE_PATTERN.test(value)
152
- ) {
153
- return null;
82
+ return { name: error.name, message: error.message };
154
83
  }
155
84
 
156
- const parsed = new Date(value);
157
- if (
158
- !PolicyDateUtils.isValid(parsed) ||
159
- parsed.toISOString() !== value
160
- ) {
161
- return Result.err("Invalid ISO 8601 UTC date string");
162
- }
163
-
164
- return Result.ok(parsed);
85
+ return { name: "NonErrorThrown", message: String(error) };
165
86
  }
166
87
 
167
- private static evaluateRelationalNumbers(
168
- actual: number,
169
- op: RelationalOperator,
170
- expected: number,
171
- ): boolean {
172
- switch (op) {
173
- case "gt":
174
- return actual > expected;
175
- case "gte":
176
- return actual >= expected;
177
- case "lt":
178
- return actual < expected;
179
- case "lte":
180
- return actual <= expected;
181
- }
88
+ private static invalidDateOperandMessage(node: ConditionLeafNode): string {
89
+ return `${INVALID_DATE_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires Date or ISO 8601 UTC (${CANONICAL_UTC_DATE_FORMAT}) string operands`;
182
90
  }
183
91
 
184
92
  private buildReport(params: {
@@ -197,93 +105,27 @@ export class ConditionEvaluatorV1 {
197
105
  };
198
106
  }
199
107
 
200
- private conditionEvalErr<TValue>(
201
- node: ConditionNode,
202
- error: unknown,
203
- ): Result<TValue, string> {
204
- const cause = ConditionEvaluatorV1.describeError(error);
205
- const message = `${CONDITION_EVAL_THROWN_TAG}: ${cause.name}: ${cause.message}`;
206
-
207
- this.options.reporter.error(
208
- this.buildReport({
209
- level: "error",
210
- tag: CONDITION_EVAL_THROWN_TAG,
211
- message,
212
- details: { node, cause },
213
- }),
214
- );
215
-
216
- return Result.err(message);
217
- }
218
-
219
- private static containsInvalidDate(value: unknown): boolean {
220
- if (value instanceof Date) {
221
- return !PolicyDateUtils.isValid(value);
222
- }
223
- if (Array.isArray(value)) {
224
- return value.some((item) =>
225
- ConditionEvaluatorV1.containsInvalidDate(item),
226
- );
227
- }
228
- return false;
229
- }
230
-
231
- // Valid Dates become their ISO string so equality/set operators compare
232
- // instants instead of references; every other value passes through.
233
- private static normalizeDateOperand(value: unknown): unknown {
234
- if (value instanceof Date) {
235
- return value.toISOString();
236
- }
237
- if (Array.isArray(value)) {
238
- return value.map((item) =>
239
- ConditionEvaluatorV1.normalizeDateOperand(item),
240
- );
241
- }
242
- return value;
243
- }
244
-
245
- private reportDateOperandError(
246
- node: ConditionLeafNode,
247
- actual: unknown,
248
- ): Result<boolean, string> {
249
- const message =
250
- ConditionEvaluatorV1.buildInvalidDateOperandMessage(node);
251
-
252
- this.options.reporter.error(
253
- this.buildReport({
254
- level: "error",
255
- tag: INVALID_DATE_OPERAND_TAG,
256
- message,
257
- details: {
258
- field: node.field,
259
- op: node.op,
260
- actual,
261
- expected: node.value,
262
- node,
263
- },
264
- }),
265
- );
266
-
267
- return Result.err(message);
268
- }
269
-
270
- private reportInvalidNumericOperand(
108
+ // Single reporting seam for every leaf-level operand error. Collapses the
109
+ // formerly per-tag report methods, whose only real difference was the tag,
110
+ // message, and (for the nullish cases) an `allowNull` detail.
111
+ private reportLeafError(
112
+ tag: ConditionEvaluationReport["tag"],
113
+ message: string,
271
114
  node: ConditionLeafNode,
272
115
  actual: unknown,
116
+ extra?: Readonly<Record<string, unknown>>,
273
117
  ): Result<boolean, string> {
274
- const message =
275
- ConditionEvaluatorV1.buildInvalidNumericOperandMessage(node);
276
-
277
118
  this.options.reporter.error(
278
119
  this.buildReport({
279
120
  level: "error",
280
- tag: INVALID_NUMERIC_OPERAND_TAG,
121
+ tag,
281
122
  message,
282
123
  details: {
283
124
  field: node.field,
284
125
  op: node.op,
285
126
  actual,
286
127
  expected: node.value,
128
+ ...extra,
287
129
  node,
288
130
  },
289
131
  }),
@@ -292,25 +134,19 @@ export class ConditionEvaluatorV1 {
292
134
  return Result.err(message);
293
135
  }
294
136
 
295
- private reportInvalidSetOperand(
296
- node: ConditionLeafNode,
297
- actual: unknown,
298
- ): Result<boolean, string> {
299
- const message =
300
- ConditionEvaluatorV1.buildInvalidSetOperandMessage(node);
137
+ private conditionEvalErr<TValue>(
138
+ node: ConditionNode,
139
+ error: unknown,
140
+ ): Result<TValue, string> {
141
+ const cause = ConditionEvaluatorV1.describeError(error);
142
+ const message = `${CONDITION_EVAL_THROWN_TAG}: ${cause.name}: ${cause.message}`;
301
143
 
302
144
  this.options.reporter.error(
303
145
  this.buildReport({
304
146
  level: "error",
305
- tag: INVALID_SET_OPERAND_TAG,
147
+ tag: CONDITION_EVAL_THROWN_TAG,
306
148
  message,
307
- details: {
308
- field: node.field,
309
- op: node.op,
310
- actual,
311
- expected: node.value,
312
- node,
313
- },
149
+ details: { node, cause },
314
150
  }),
315
151
  );
316
152
 
@@ -325,84 +161,49 @@ export class ConditionEvaluatorV1 {
325
161
  return null;
326
162
  }
327
163
 
328
- const actualDateResult =
329
- ConditionEvaluatorV1.parseComparableDate(actual);
330
- const expectedDateResult = ConditionEvaluatorV1.parseComparableDate(
331
- node.value,
332
- );
164
+ const actualDate = parseComparableDate(actual);
165
+ const expectedDate = parseComparableDate(node.value);
333
166
 
334
- if (actualDateResult === null && expectedDateResult === null) {
167
+ // Neither operand is date-shaped: let the numeric path handle it.
168
+ if (actualDate === null && expectedDate === null) {
335
169
  return null;
336
170
  }
337
171
 
338
172
  const allowsNull = node.allowNull === true;
339
-
340
- if (actual === null) {
341
- if (allowsNull) {
342
- return Result.ok(false);
343
- }
344
-
345
- const message = ConditionEvaluatorV1.buildNullishDateOperandMessage(
346
- node,
347
- actual,
348
- );
349
- this.options.reporter.error(
350
- this.buildReport({
351
- level: "error",
352
- tag: NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG,
353
- message,
354
- details: {
355
- field: node.field,
356
- op: node.op,
357
- actual,
358
- expected: node.value,
359
- allowNull: allowsNull,
360
- node,
361
- },
362
- }),
363
- );
364
-
365
- return Result.err(message);
173
+ if (actual === null && allowsNull) {
174
+ return Result.ok(false);
366
175
  }
367
176
 
368
- if (actual === undefined) {
369
- const message = ConditionEvaluatorV1.buildNullishDateOperandMessage(
177
+ if (actual === null || actual === undefined) {
178
+ const resolved = actual === null ? "null" : "undefined";
179
+ return this.reportLeafError(
180
+ NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG,
181
+ `${NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG}: "${node.field}" resolved to ${resolved} for date comparison operator "${node.op}"`,
370
182
  node,
371
183
  actual,
184
+ { allowNull: allowsNull },
372
185
  );
373
- this.options.reporter.error(
374
- this.buildReport({
375
- level: "error",
376
- tag: NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG,
377
- message,
378
- details: {
379
- field: node.field,
380
- op: node.op,
381
- actual,
382
- expected: node.value,
383
- allowNull: allowsNull,
384
- node,
385
- },
386
- }),
387
- );
388
-
389
- return Result.err(message);
390
186
  }
391
187
 
392
188
  if (
393
- actualDateResult === null ||
394
- actualDateResult.isErr() ||
395
- expectedDateResult === null ||
396
- expectedDateResult.isErr()
189
+ actualDate === null ||
190
+ actualDate.isErr() ||
191
+ expectedDate === null ||
192
+ expectedDate.isErr()
397
193
  ) {
398
- return this.reportDateOperandError(node, actual);
194
+ return this.reportLeafError(
195
+ INVALID_DATE_OPERAND_TAG,
196
+ ConditionEvaluatorV1.invalidDateOperandMessage(node),
197
+ node,
198
+ actual,
199
+ );
399
200
  }
400
201
 
401
202
  return Result.ok(
402
- ConditionEvaluatorV1.evaluateRelationalNumbers(
403
- actualDateResult.getOrNull()!.getTime(),
203
+ evaluateRelationalNumbers(
204
+ actualDate.getOrNull()!.getTime(),
404
205
  node.op,
405
- expectedDateResult.getOrNull()!.getTime(),
206
+ expectedDate.getOrNull()!.getTime(),
406
207
  ),
407
208
  );
408
209
  }
@@ -418,51 +219,20 @@ export class ConditionEvaluatorV1 {
418
219
  case "neq":
419
220
  return actual !== expected;
420
221
  case "gt":
421
- return (
422
- typeof actual === "number" &&
423
- typeof expected === "number" &&
424
- ConditionEvaluatorV1.evaluateRelationalNumbers(
425
- actual,
426
- op,
427
- expected,
428
- )
429
- );
430
222
  case "gte":
431
- return (
432
- typeof actual === "number" &&
433
- typeof expected === "number" &&
434
- ConditionEvaluatorV1.evaluateRelationalNumbers(
435
- actual,
436
- op,
437
- expected,
438
- )
439
- );
440
223
  case "lt":
441
- return (
442
- typeof actual === "number" &&
443
- typeof expected === "number" &&
444
- ConditionEvaluatorV1.evaluateRelationalNumbers(
445
- actual,
446
- op,
447
- expected,
448
- )
449
- );
450
224
  case "lte":
451
225
  return (
452
226
  typeof actual === "number" &&
453
227
  typeof expected === "number" &&
454
- ConditionEvaluatorV1.evaluateRelationalNumbers(
455
- actual,
456
- op,
457
- expected,
458
- )
228
+ evaluateRelationalNumbers(actual, op, expected)
459
229
  );
460
230
  case "in":
461
231
  return Array.isArray(expected) && expected.includes(actual);
462
232
  case "notIn":
463
233
  return Array.isArray(expected) && !expected.includes(actual);
464
234
  case "isNull":
465
- return actual === null;
235
+ return actual === null || actual === undefined;
466
236
  case "isNotNull":
467
237
  return actual !== null && actual !== undefined;
468
238
  }
@@ -474,29 +244,14 @@ export class ConditionEvaluatorV1 {
474
244
  ): Result<boolean, string> {
475
245
  const allowsNull = node.allowNull === true;
476
246
  if (actual === undefined || (actual === null && !allowsNull)) {
477
- const message =
478
- ConditionEvaluatorV1.buildNullishNumericOperandMessage(
479
- node,
480
- actual as null | undefined,
481
- );
482
-
483
- this.options.reporter.error(
484
- this.buildReport({
485
- level: "error",
486
- tag: NULLISH_NUMERIC_OPERAND_NOT_ALLOWED_TAG,
487
- message,
488
- details: {
489
- field: node.field,
490
- op: node.op,
491
- actual,
492
- expected: node.value,
493
- allowNull: allowsNull,
494
- node,
495
- },
496
- }),
247
+ const resolved = actual === null ? "null" : "undefined";
248
+ return this.reportLeafError(
249
+ NULLISH_NUMERIC_OPERAND_NOT_ALLOWED_TAG,
250
+ `${NULLISH_NUMERIC_OPERAND_NOT_ALLOWED_TAG}: "${node.field}" resolved to ${resolved} for numeric operator "${node.op}"`,
251
+ node,
252
+ actual,
253
+ { allowNull: allowsNull },
497
254
  );
498
-
499
- return Result.err(message);
500
255
  }
501
256
 
502
257
  // null + allowNull: a relational comparison against a missing value
@@ -505,11 +260,21 @@ export class ConditionEvaluatorV1 {
505
260
  return Result.ok(false);
506
261
  }
507
262
 
508
- // Both operands must be numbers. A present but wrong-typed operand is a
509
- // context/configuration error, surfaced like the date path instead of
510
- // silently collapsing to "no match".
511
- if (typeof actual !== "number" || typeof node.value !== "number") {
512
- return this.reportInvalidNumericOperand(node, actual);
263
+ // Both operands must be finite numbers. A present but wrong-typed or
264
+ // NaN operand is a context/configuration error, surfaced like the date
265
+ // path instead of silently collapsing to "no match".
266
+ if (
267
+ typeof actual !== "number" ||
268
+ Number.isNaN(actual) ||
269
+ typeof node.value !== "number" ||
270
+ Number.isNaN(node.value)
271
+ ) {
272
+ return this.reportLeafError(
273
+ INVALID_NUMERIC_OPERAND_TAG,
274
+ `${INVALID_NUMERIC_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires numeric operands`,
275
+ node,
276
+ actual,
277
+ );
513
278
  }
514
279
 
515
280
  return Result.ok(this.evaluateOperator(actual, node.op, node.value));
@@ -521,18 +286,17 @@ export class ConditionEvaluatorV1 {
521
286
  node.field,
522
287
  );
523
288
  if (actualResult.isErr()) {
289
+ const message = `MISSING_CONTEXT_FIELD: "${node.field}" not found in context`;
524
290
  this.options.reporter.warn(
525
291
  this.buildReport({
526
292
  level: "warn",
527
293
  tag: "MISSING_CONTEXT_FIELD",
528
- message: `MISSING_CONTEXT_FIELD: "${node.field}" not found in context`,
294
+ message,
529
295
  details: { field: node.field, node },
530
296
  }),
531
297
  );
532
298
 
533
- return Result.err(
534
- `MISSING_CONTEXT_FIELD: "${node.field}" not found in context`,
535
- );
299
+ return Result.err(message);
536
300
  }
537
301
 
538
302
  try {
@@ -553,7 +317,12 @@ export class ConditionEvaluatorV1 {
553
317
  (node.op === "in" || node.op === "notIn") &&
554
318
  !Array.isArray(node.value)
555
319
  ) {
556
- return this.reportInvalidSetOperand(node, actual);
320
+ return this.reportLeafError(
321
+ INVALID_SET_OPERAND_TAG,
322
+ `${INVALID_SET_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires an array operand`,
323
+ node,
324
+ actual,
325
+ );
557
326
  }
558
327
 
559
328
  if (
@@ -567,17 +336,22 @@ export class ConditionEvaluatorV1 {
567
336
  // including set items) to their ISO string; an invalid Date is
568
337
  // a context/configuration error, like the relational path.
569
338
  if (
570
- ConditionEvaluatorV1.containsInvalidDate(actual) ||
571
- ConditionEvaluatorV1.containsInvalidDate(node.value)
339
+ containsInvalidDate(actual) ||
340
+ containsInvalidDate(node.value)
572
341
  ) {
573
- return this.reportDateOperandError(node, actual);
342
+ return this.reportLeafError(
343
+ INVALID_DATE_OPERAND_TAG,
344
+ ConditionEvaluatorV1.invalidDateOperandMessage(node),
345
+ node,
346
+ actual,
347
+ );
574
348
  }
575
349
 
576
350
  return Result.ok(
577
351
  this.evaluateOperator(
578
- ConditionEvaluatorV1.normalizeDateOperand(actual),
352
+ normalizeDateOperand(actual),
579
353
  node.op,
580
- ConditionEvaluatorV1.normalizeDateOperand(node.value),
354
+ normalizeDateOperand(node.value),
581
355
  ),
582
356
  );
583
357
  }
@@ -632,7 +406,7 @@ export class ConditionEvaluatorV1 {
632
406
  if (ConditionEvaluatorV1.isAndNode(node)) {
633
407
  if (node.and.length === 0) {
634
408
  return Result.err(
635
- ConditionEvaluatorV1.buildEmptyAndConditionMessage(),
409
+ `${EMPTY_AND_CONDITION_TAG}: AND nodes must contain at least one child condition`,
636
410
  );
637
411
  }
638
412
 
@@ -697,7 +471,7 @@ export class ConditionEvaluatorV1 {
697
471
 
698
472
  if (lastTrace === null) {
699
473
  return Result.err(
700
- ConditionEvaluatorV1.buildEmptyOrConditionMessage(),
474
+ `${EMPTY_OR_CONDITION_TAG}: OR nodes must contain at least one child condition`,
701
475
  );
702
476
  }
703
477
 
@@ -28,23 +28,27 @@ export const conditionLeafNodeSchema: z.ZodType<ConditionLeafNode> = z
28
28
  })
29
29
  .strict() as z.ZodType<ConditionLeafNode>;
30
30
 
31
- export const conditionNodeSchema: z.ZodType<ConditionNode> = z.lazy(() =>
32
- z.union([
31
+ // Cap nesting depth so an adversarially deep payload fails validation instead
32
+ // of overflowing the stack during parse: zod recurses once per level, and the
33
+ // old `z.lazy` schema was unbounded. The schema is built to exactly this depth
34
+ // (leaf-only past it), so a too-deep tree is rejected at the boundary without
35
+ // deep recursion. 32 is far beyond any hand-written policy — raise it only with
36
+ // a matching stack-safety review.
37
+ export const MAX_CONDITION_DEPTH = 32;
38
+
39
+ function boundedConditionNodeSchema(depth: number): z.ZodType<ConditionNode> {
40
+ if (depth <= 1) {
41
+ return conditionLeafNodeSchema;
42
+ }
43
+
44
+ const child = boundedConditionNodeSchema(depth - 1);
45
+ return z.union([
33
46
  conditionLeafNodeSchema,
34
- z
35
- .object({
36
- and: z.array(conditionNodeSchema).min(1),
37
- })
38
- .strict(),
39
- z
40
- .object({
41
- or: z.array(conditionNodeSchema).min(1),
42
- })
43
- .strict(),
44
- z
45
- .object({
46
- not: conditionNodeSchema,
47
- })
48
- .strict(),
49
- ]),
50
- );
47
+ z.object({ and: z.array(child).min(1) }).strict(),
48
+ z.object({ or: z.array(child).min(1) }).strict(),
49
+ z.object({ not: child }).strict(),
50
+ ]) as z.ZodType<ConditionNode>;
51
+ }
52
+
53
+ export const conditionNodeSchema: z.ZodType<ConditionNode> =
54
+ boundedConditionNodeSchema(MAX_CONDITION_DEPTH);
@@ -3,17 +3,24 @@
3
3
  // share the same condition-tree DSL. Keeping them here avoids making compute/v1
4
4
  // structurally dependent on gate/v1.
5
5
 
6
- export type ConditionOp =
7
- | "eq"
8
- | "neq"
9
- | "gt"
10
- | "gte"
11
- | "lt"
12
- | "lte"
13
- | "in"
14
- | "notIn"
15
- | "isNull"
16
- | "isNotNull";
6
+ // Single source of truth for the operator vocabulary: the union is derived from
7
+ // this array so a new operator is added in exactly one place, and consumers that
8
+ // need the runtime list (e.g. AbacRule's structural check) reuse it instead of
9
+ // hand-maintaining a parallel set that can drift out of sync.
10
+ export const CONDITION_OPS = [
11
+ "eq",
12
+ "neq",
13
+ "gt",
14
+ "gte",
15
+ "lt",
16
+ "lte",
17
+ "in",
18
+ "notIn",
19
+ "isNull",
20
+ "isNotNull",
21
+ ] as const;
22
+
23
+ export type ConditionOp = (typeof CONDITION_OPS)[number];
17
24
 
18
25
  export interface ConditionLeafNode {
19
26
  readonly field: string;