@cullet/erp-core 1.4.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 +7 -1
  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 +21 -6
  9. package/dist/app-error.cjs.map +1 -1
  10. package/dist/app-error.js +21 -6
  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 +117 -172
  27. package/dist/condition-evaluator.cjs.map +1 -1
  28. package/dist/condition-evaluator.js +118 -167
  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 +3 -2
  51. package/dist/errors/index.d.cts +1 -1
  52. package/dist/errors/index.d.ts +1 -1
  53. package/dist/errors/index.js +3 -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 +2 -44
  67. package/dist/hashing.cjs.map +1 -1
  68. package/dist/hashing.js +3 -39
  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 +15 -11
  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 +8 -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 -13
  109. package/dist/policies/engines/v1/gate/index.d.ts +3 -13
  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 +41 -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 +43 -48
  123. package/dist/policy-service.js.map +1 -1
  124. package/dist/requested-by.cjs +4 -4
  125. package/dist/requested-by.cjs.map +1 -1
  126. package/dist/requested-by.js +2 -2
  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 +87 -0
  143. package/dist/stable-stringify.cjs.map +1 -0
  144. package/dist/stable-stringify.js +76 -0
  145. package/dist/stable-stringify.js.map +1 -0
  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 +174 -16
  149. package/dist/temporal-use-case.cjs.map +1 -1
  150. package/dist/temporal-use-case.d.cts +82 -44
  151. package/dist/temporal-use-case.d.ts +82 -44
  152. package/dist/temporal-use-case.js +167 -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 +141 -8
  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 -8
  163. package/dist/uuid-identifier.js.map +1 -1
  164. package/dist/uuid.cjs +23 -0
  165. package/dist/uuid.cjs.map +1 -0
  166. package/dist/uuid.js +18 -0
  167. package/dist/uuid.js.map +1 -0
  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 +29 -8
  177. package/dist/validation-error.d.ts +29 -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 +23 -4
  195. package/dist/value-object.cjs.map +1 -1
  196. package/dist/value-object.d.cts +25 -1
  197. package/dist/value-object.d.ts +25 -1
  198. package/dist/value-object.js +23 -4
  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 +5 -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 +13 -5
  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 +25 -3
  218. package/src/core/application/temporal/temporal-use-case.ts +31 -4
  219. package/src/core/application/use-case.ts +45 -8
  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 +51 -8
  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 +6 -11
  232. package/src/core/domain/value-object.ts +29 -3
  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 +35 -12
  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 +120 -291
  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 -57
  281. package/src/core/shared/immutable.ts +22 -4
  282. package/src/core/shared/stable-stringify.ts +144 -0
  283. package/src/core/shared/uuid.ts +16 -0
  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 -126
  300. package/dist/entity.cjs.map +0 -1
  301. package/dist/entity.js +0 -115
  302. package/dist/entity.js.map +0 -1
  303. package/dist/use-case.cjs +0 -96
  304. package/dist/use-case.cjs.map +0 -1
  305. package/dist/use-case.js +0 -91
  306. package/dist/use-case.js.map +0 -1
@@ -1,7 +1,14 @@
1
1
  import { PolicyContextPath } from "../../context/index.js";
2
- import { PolicyDateUtils } from "../../utils/index.js";
3
2
  import { Result } from "../../../result/result.js";
4
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";
5
12
  import type {
6
13
  ConditionAndNode,
7
14
  ConditionLeafNode,
@@ -25,8 +32,6 @@ const INVALID_NUMERIC_OPERAND_TAG = "INVALID_NUMERIC_OPERAND";
25
32
  const INVALID_SET_OPERAND_TAG = "INVALID_SET_OPERAND";
26
33
  const EMPTY_OR_CONDITION_TAG = "EMPTY_OR_CONDITION";
27
34
  const EMPTY_AND_CONDITION_TAG = "EMPTY_AND_CONDITION";
28
- const ISO_8601_UTC_DATE_PATTERN =
29
- /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
30
35
 
31
36
  export interface ConditionEvaluationTrace {
32
37
  readonly conditionPath: string;
@@ -41,8 +46,6 @@ export interface TracedConditionEvaluation {
41
46
  readonly trace: ConditionEvaluationTrace;
42
47
  }
43
48
 
44
- type RelationalOperator = Extract<ConditionOp, "gt" | "gte" | "lt" | "lte">;
45
-
46
49
  export class ConditionEvaluatorV1 {
47
50
  constructor(
48
51
  private readonly context: PolicyContext,
@@ -76,106 +79,14 @@ export class ConditionEvaluatorV1 {
76
79
  message: string;
77
80
  } {
78
81
  if (error instanceof Error) {
79
- return {
80
- name: error.name,
81
- message: error.message,
82
- };
83
- }
84
-
85
- return {
86
- name: "NonErrorThrown",
87
- message: String(error),
88
- };
89
- }
90
-
91
- private static buildNullishNumericOperandMessage(
92
- node: ConditionLeafNode,
93
- actual: null | undefined,
94
- ): string {
95
- const resolvedValue = actual === null ? "null" : "undefined";
96
-
97
- return `${NULLISH_NUMERIC_OPERAND_NOT_ALLOWED_TAG}: "${node.field}" resolved to ${resolvedValue} for numeric operator "${node.op}"`;
98
- }
99
-
100
- private static buildNullishDateOperandMessage(
101
- node: ConditionLeafNode,
102
- actual: null | undefined,
103
- ): string {
104
- const resolvedValue = actual === null ? "null" : "undefined";
105
-
106
- return `${NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG}: "${node.field}" resolved to ${resolvedValue} for date comparison operator "${node.op}"`;
107
- }
108
-
109
- private static buildInvalidDateOperandMessage(
110
- node: ConditionLeafNode,
111
- ): string {
112
- return `${INVALID_DATE_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires Date or ISO 8601 UTC string operands`;
113
- }
114
-
115
- private static buildInvalidNumericOperandMessage(
116
- node: ConditionLeafNode,
117
- ): string {
118
- return `${INVALID_NUMERIC_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires numeric operands`;
119
- }
120
-
121
- private static buildInvalidSetOperandMessage(
122
- node: ConditionLeafNode,
123
- ): string {
124
- return `${INVALID_SET_OPERAND_TAG}: "${node.field}" with operator "${node.op}" requires an array operand`;
125
- }
126
-
127
- private static buildEmptyOrConditionMessage(): string {
128
- return `${EMPTY_OR_CONDITION_TAG}: OR nodes must contain at least one child condition`;
129
- }
130
-
131
- private static buildEmptyAndConditionMessage(): string {
132
- return `${EMPTY_AND_CONDITION_TAG}: AND nodes must contain at least one child condition`;
133
- }
134
-
135
- private static parseComparableDate(
136
- value: unknown,
137
- ): Result<Date, string> | null {
138
- if (value instanceof Date) {
139
- if (!PolicyDateUtils.isValid(value)) {
140
- return Result.err("Invalid Date instance");
141
- }
142
-
143
- return Result.ok(value);
144
- }
145
-
146
- if (
147
- typeof value !== "string" ||
148
- !ISO_8601_UTC_DATE_PATTERN.test(value)
149
- ) {
150
- return null;
82
+ return { name: error.name, message: error.message };
151
83
  }
152
84
 
153
- const parsed = new Date(value);
154
- if (
155
- !PolicyDateUtils.isValid(parsed) ||
156
- parsed.toISOString() !== value
157
- ) {
158
- return Result.err("Invalid ISO 8601 UTC date string");
159
- }
160
-
161
- return Result.ok(parsed);
85
+ return { name: "NonErrorThrown", message: String(error) };
162
86
  }
163
87
 
164
- private static evaluateRelationalNumbers(
165
- actual: number,
166
- op: RelationalOperator,
167
- expected: number,
168
- ): boolean {
169
- switch (op) {
170
- case "gt":
171
- return actual > expected;
172
- case "gte":
173
- return actual >= expected;
174
- case "lt":
175
- return actual < expected;
176
- case "lte":
177
- return actual <= expected;
178
- }
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`;
179
90
  }
180
91
 
181
92
  private buildReport(params: {
@@ -194,42 +105,27 @@ export class ConditionEvaluatorV1 {
194
105
  };
195
106
  }
196
107
 
197
- private conditionEvalErr<TValue>(
198
- node: ConditionNode,
199
- error: unknown,
200
- ): Result<TValue, string> {
201
- const cause = ConditionEvaluatorV1.describeError(error);
202
- const message = `${CONDITION_EVAL_THROWN_TAG}: ${cause.name}: ${cause.message}`;
203
-
204
- this.options.reporter.error(
205
- this.buildReport({
206
- level: "error",
207
- tag: CONDITION_EVAL_THROWN_TAG,
208
- message,
209
- details: { node, cause },
210
- }),
211
- );
212
-
213
- return Result.err(message);
214
- }
215
-
216
- private reportDateOperandError(
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,
217
114
  node: ConditionLeafNode,
218
115
  actual: unknown,
116
+ extra?: Readonly<Record<string, unknown>>,
219
117
  ): Result<boolean, string> {
220
- const message =
221
- ConditionEvaluatorV1.buildInvalidDateOperandMessage(node);
222
-
223
118
  this.options.reporter.error(
224
119
  this.buildReport({
225
120
  level: "error",
226
- tag: INVALID_DATE_OPERAND_TAG,
121
+ tag,
227
122
  message,
228
123
  details: {
229
124
  field: node.field,
230
125
  op: node.op,
231
126
  actual,
232
127
  expected: node.value,
128
+ ...extra,
233
129
  node,
234
130
  },
235
131
  }),
@@ -238,50 +134,19 @@ export class ConditionEvaluatorV1 {
238
134
  return Result.err(message);
239
135
  }
240
136
 
241
- private reportInvalidNumericOperand(
242
- node: ConditionLeafNode,
243
- actual: unknown,
244
- ): Result<boolean, string> {
245
- const message =
246
- ConditionEvaluatorV1.buildInvalidNumericOperandMessage(node);
247
-
248
- this.options.reporter.error(
249
- this.buildReport({
250
- level: "error",
251
- tag: INVALID_NUMERIC_OPERAND_TAG,
252
- message,
253
- details: {
254
- field: node.field,
255
- op: node.op,
256
- actual,
257
- expected: node.value,
258
- node,
259
- },
260
- }),
261
- );
262
-
263
- return Result.err(message);
264
- }
265
-
266
- private reportInvalidSetOperand(
267
- node: ConditionLeafNode,
268
- actual: unknown,
269
- ): Result<boolean, string> {
270
- const message =
271
- 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}`;
272
143
 
273
144
  this.options.reporter.error(
274
145
  this.buildReport({
275
146
  level: "error",
276
- tag: INVALID_SET_OPERAND_TAG,
147
+ tag: CONDITION_EVAL_THROWN_TAG,
277
148
  message,
278
- details: {
279
- field: node.field,
280
- op: node.op,
281
- actual,
282
- expected: node.value,
283
- node,
284
- },
149
+ details: { node, cause },
285
150
  }),
286
151
  );
287
152
 
@@ -296,84 +161,49 @@ export class ConditionEvaluatorV1 {
296
161
  return null;
297
162
  }
298
163
 
299
- const actualDateResult =
300
- ConditionEvaluatorV1.parseComparableDate(actual);
301
- const expectedDateResult = ConditionEvaluatorV1.parseComparableDate(
302
- node.value,
303
- );
164
+ const actualDate = parseComparableDate(actual);
165
+ const expectedDate = parseComparableDate(node.value);
304
166
 
305
- if (actualDateResult === null && expectedDateResult === null) {
167
+ // Neither operand is date-shaped: let the numeric path handle it.
168
+ if (actualDate === null && expectedDate === null) {
306
169
  return null;
307
170
  }
308
171
 
309
172
  const allowsNull = node.allowNull === true;
310
-
311
- if (actual === null) {
312
- if (allowsNull) {
313
- return Result.ok(false);
314
- }
315
-
316
- const message = ConditionEvaluatorV1.buildNullishDateOperandMessage(
317
- node,
318
- actual,
319
- );
320
- this.options.reporter.error(
321
- this.buildReport({
322
- level: "error",
323
- tag: NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG,
324
- message,
325
- details: {
326
- field: node.field,
327
- op: node.op,
328
- actual,
329
- expected: node.value,
330
- allowNull: allowsNull,
331
- node,
332
- },
333
- }),
334
- );
335
-
336
- return Result.err(message);
173
+ if (actual === null && allowsNull) {
174
+ return Result.ok(false);
337
175
  }
338
176
 
339
- if (actual === undefined) {
340
- 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}"`,
341
182
  node,
342
183
  actual,
184
+ { allowNull: allowsNull },
343
185
  );
344
- this.options.reporter.error(
345
- this.buildReport({
346
- level: "error",
347
- tag: NULLISH_DATE_OPERAND_NOT_ALLOWED_TAG,
348
- message,
349
- details: {
350
- field: node.field,
351
- op: node.op,
352
- actual,
353
- expected: node.value,
354
- allowNull: allowsNull,
355
- node,
356
- },
357
- }),
358
- );
359
-
360
- return Result.err(message);
361
186
  }
362
187
 
363
188
  if (
364
- actualDateResult === null ||
365
- actualDateResult.isErr() ||
366
- expectedDateResult === null ||
367
- expectedDateResult.isErr()
189
+ actualDate === null ||
190
+ actualDate.isErr() ||
191
+ expectedDate === null ||
192
+ expectedDate.isErr()
368
193
  ) {
369
- return this.reportDateOperandError(node, actual);
194
+ return this.reportLeafError(
195
+ INVALID_DATE_OPERAND_TAG,
196
+ ConditionEvaluatorV1.invalidDateOperandMessage(node),
197
+ node,
198
+ actual,
199
+ );
370
200
  }
371
201
 
372
202
  return Result.ok(
373
- ConditionEvaluatorV1.evaluateRelationalNumbers(
374
- actualDateResult.getOrNull()!.getTime(),
203
+ evaluateRelationalNumbers(
204
+ actualDate.getOrNull()!.getTime(),
375
205
  node.op,
376
- expectedDateResult.getOrNull()!.getTime(),
206
+ expectedDate.getOrNull()!.getTime(),
377
207
  ),
378
208
  );
379
209
  }
@@ -389,51 +219,20 @@ export class ConditionEvaluatorV1 {
389
219
  case "neq":
390
220
  return actual !== expected;
391
221
  case "gt":
392
- return (
393
- typeof actual === "number" &&
394
- typeof expected === "number" &&
395
- ConditionEvaluatorV1.evaluateRelationalNumbers(
396
- actual,
397
- op,
398
- expected,
399
- )
400
- );
401
222
  case "gte":
402
- return (
403
- typeof actual === "number" &&
404
- typeof expected === "number" &&
405
- ConditionEvaluatorV1.evaluateRelationalNumbers(
406
- actual,
407
- op,
408
- expected,
409
- )
410
- );
411
223
  case "lt":
412
- return (
413
- typeof actual === "number" &&
414
- typeof expected === "number" &&
415
- ConditionEvaluatorV1.evaluateRelationalNumbers(
416
- actual,
417
- op,
418
- expected,
419
- )
420
- );
421
224
  case "lte":
422
225
  return (
423
226
  typeof actual === "number" &&
424
227
  typeof expected === "number" &&
425
- ConditionEvaluatorV1.evaluateRelationalNumbers(
426
- actual,
427
- op,
428
- expected,
429
- )
228
+ evaluateRelationalNumbers(actual, op, expected)
430
229
  );
431
230
  case "in":
432
231
  return Array.isArray(expected) && expected.includes(actual);
433
232
  case "notIn":
434
233
  return Array.isArray(expected) && !expected.includes(actual);
435
234
  case "isNull":
436
- return actual === null;
235
+ return actual === null || actual === undefined;
437
236
  case "isNotNull":
438
237
  return actual !== null && actual !== undefined;
439
238
  }
@@ -445,29 +244,14 @@ export class ConditionEvaluatorV1 {
445
244
  ): Result<boolean, string> {
446
245
  const allowsNull = node.allowNull === true;
447
246
  if (actual === undefined || (actual === null && !allowsNull)) {
448
- const message =
449
- ConditionEvaluatorV1.buildNullishNumericOperandMessage(
450
- node,
451
- actual as null | undefined,
452
- );
453
-
454
- this.options.reporter.error(
455
- this.buildReport({
456
- level: "error",
457
- tag: NULLISH_NUMERIC_OPERAND_NOT_ALLOWED_TAG,
458
- message,
459
- details: {
460
- field: node.field,
461
- op: node.op,
462
- actual,
463
- expected: node.value,
464
- allowNull: allowsNull,
465
- node,
466
- },
467
- }),
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 },
468
254
  );
469
-
470
- return Result.err(message);
471
255
  }
472
256
 
473
257
  // null + allowNull: a relational comparison against a missing value
@@ -476,11 +260,21 @@ export class ConditionEvaluatorV1 {
476
260
  return Result.ok(false);
477
261
  }
478
262
 
479
- // Both operands must be numbers. A present but wrong-typed operand is a
480
- // context/configuration error, surfaced like the date path instead of
481
- // silently collapsing to "no match".
482
- if (typeof actual !== "number" || typeof node.value !== "number") {
483
- 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
+ );
484
278
  }
485
279
 
486
280
  return Result.ok(this.evaluateOperator(actual, node.op, node.value));
@@ -492,18 +286,17 @@ export class ConditionEvaluatorV1 {
492
286
  node.field,
493
287
  );
494
288
  if (actualResult.isErr()) {
289
+ const message = `MISSING_CONTEXT_FIELD: "${node.field}" not found in context`;
495
290
  this.options.reporter.warn(
496
291
  this.buildReport({
497
292
  level: "warn",
498
293
  tag: "MISSING_CONTEXT_FIELD",
499
- message: `MISSING_CONTEXT_FIELD: "${node.field}" not found in context`,
294
+ message,
500
295
  details: { field: node.field, node },
501
296
  }),
502
297
  );
503
298
 
504
- return Result.err(
505
- `MISSING_CONTEXT_FIELD: "${node.field}" not found in context`,
506
- );
299
+ return Result.err(message);
507
300
  }
508
301
 
509
302
  try {
@@ -524,7 +317,43 @@ export class ConditionEvaluatorV1 {
524
317
  (node.op === "in" || node.op === "notIn") &&
525
318
  !Array.isArray(node.value)
526
319
  ) {
527
- 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
+ );
326
+ }
327
+
328
+ if (
329
+ node.op === "eq" ||
330
+ node.op === "neq" ||
331
+ node.op === "in" ||
332
+ node.op === "notIn"
333
+ ) {
334
+ // A Date operand would otherwise compare by reference and
335
+ // silently never match. Normalize valid Dates (either side,
336
+ // including set items) to their ISO string; an invalid Date is
337
+ // a context/configuration error, like the relational path.
338
+ if (
339
+ containsInvalidDate(actual) ||
340
+ containsInvalidDate(node.value)
341
+ ) {
342
+ return this.reportLeafError(
343
+ INVALID_DATE_OPERAND_TAG,
344
+ ConditionEvaluatorV1.invalidDateOperandMessage(node),
345
+ node,
346
+ actual,
347
+ );
348
+ }
349
+
350
+ return Result.ok(
351
+ this.evaluateOperator(
352
+ normalizeDateOperand(actual),
353
+ node.op,
354
+ normalizeDateOperand(node.value),
355
+ ),
356
+ );
528
357
  }
529
358
 
530
359
  return Result.ok(
@@ -577,7 +406,7 @@ export class ConditionEvaluatorV1 {
577
406
  if (ConditionEvaluatorV1.isAndNode(node)) {
578
407
  if (node.and.length === 0) {
579
408
  return Result.err(
580
- ConditionEvaluatorV1.buildEmptyAndConditionMessage(),
409
+ `${EMPTY_AND_CONDITION_TAG}: AND nodes must contain at least one child condition`,
581
410
  );
582
411
  }
583
412
 
@@ -642,7 +471,7 @@ export class ConditionEvaluatorV1 {
642
471
 
643
472
  if (lastTrace === null) {
644
473
  return Result.err(
645
- ConditionEvaluatorV1.buildEmptyOrConditionMessage(),
474
+ `${EMPTY_OR_CONDITION_TAG}: OR nodes must contain at least one child condition`,
646
475
  );
647
476
  }
648
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;
@@ -8,12 +8,10 @@ export type {
8
8
  } from "../config/index.js";
9
9
  export { CoreConfig, coreConfig } from "../config/index.js";
10
10
 
11
- // ─── Result (technical execution) ───────────────────────────────────────────
12
- export { Err, Ok, Result } from "../result/result.js";
13
-
14
- // ─── Outcome (business decision) ───────────────────────────────────────────
15
- export type { CommonOutcomeStatus } from "../result/outcome.js";
16
- export { Outcome } from "../result/outcome.js";
11
+ // ─── Result (technical execution) + Outcome (business decision) ─────────────
12
+ export { Err, Ok, Result } from "../result/index.js";
13
+ export type { CommonOutcomeStatus } from "../result/index.js";
14
+ export { Outcome } from "../result/index.js";
17
15
 
18
16
  // ─── Utils ──────────────────────────────────────────────────────────────────
19
17
  export { PolicyHashing } from "./utils/index.js";