@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,10 +1,22 @@
1
- import { InvariantViolationException } from "../../exceptions/invariant-violation-exception.js";
2
1
  import { makeImmutable } from "../../shared/immutable.js";
3
- import { assertValidDate } from "../../shared/temporal-guards.js";
2
+
3
+ import { assertHalfOpenInterval } from "./half-open-interval.js";
4
4
 
5
5
  /**
6
6
  * Represents business time (Valid Time).
7
7
  * Defines when a piece of information is considered true or in effect in the real world.
8
+ *
9
+ * The window is half-open — `from` inclusive, `to` exclusive — and strictly
10
+ * non-empty: `to` must be later than `from`, so a zero-duration range `[t, t)`
11
+ * is deliberately not representable. Model an instantaneous event as an open
12
+ * range closed by the next fact, not as an empty window.
13
+ *
14
+ * One caveat mirrors {@link ValueObject}: `createValidTime` clones the input
15
+ * dates (no aliasing) but the inner `Date`s are NOT frozen — `Object.freeze`
16
+ * cannot block `setTime`/`setFullYear`, so a caller can still mutate `from`
17
+ * in place and corrupt the range after creation. `readonly` only prevents
18
+ * reassignment. Treat the dates as read-only, or keep instants as ISO/epoch in
19
+ * your own state when that guarantee matters.
8
20
  */
9
21
  interface ValidTime {
10
22
  /** Start of the validity window (inclusive). */
@@ -19,19 +31,12 @@ function assertValidTime(
19
31
  validTime: ValidTime,
20
32
  fieldName: string = "validTime",
21
33
  ): void {
22
- assertValidDate(`${fieldName}.from`, validTime.from);
23
-
24
- if (validTime.to === undefined) {
25
- return;
26
- }
27
-
28
- assertValidDate(`${fieldName}.to`, validTime.to);
29
-
30
- if (validTime.to.getTime() <= validTime.from.getTime()) {
31
- throw new InvariantViolationException(
32
- `${fieldName}.to must be later than ${fieldName}.from`,
33
- );
34
- }
34
+ assertHalfOpenInterval(
35
+ `${fieldName}.from`,
36
+ validTime.from,
37
+ `${fieldName}.to`,
38
+ validTime.to,
39
+ );
35
40
  }
36
41
 
37
42
  function createValidTime(input: CreateValidTimeInput): ValidTime {
@@ -1,13 +1,6 @@
1
+ import { UUID_PATTERN } from "../shared/uuid.js";
1
2
  import { ValueObject } from "./value-object.js";
2
3
 
3
- /**
4
- * Canonical RFC-4122 UUID (versions 1–5), matched case-insensitively. Declared
5
- * once here so the dozens of typed identifiers a domain accumulates never have
6
- * to re-state the pattern.
7
- */
8
- const UUID_PATTERN =
9
- /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
-
11
4
  /**
12
5
  * Base class for UUID-backed identity value objects.
13
6
  *
@@ -45,7 +38,7 @@ abstract class UuidIdentifier<
45
38
  * two identifiers with different brands are not interchangeable at the type
46
39
  * level despite sharing the same `string` value.
47
40
  */
48
- protected declare readonly __brand: TBrand;
41
+ declare protected readonly __brand: TBrand;
49
42
 
50
43
  protected constructor(value: string) {
51
44
  super(value);
@@ -61,8 +54,10 @@ abstract class UuidIdentifier<
61
54
  }
62
55
 
63
56
  /**
64
- * Whether `candidate` is a canonical UUID (versions 1–5). The single source
65
- * of truth for the format; concrete identifiers call this from `create`.
57
+ * Whether `candidate` is a canonical UUID (versions 1–8, including the
58
+ * time-ordered v7; nil and max UUIDs are rejected as sentinels). The single
59
+ * source of truth for the format — see `shared/uuid.ts`; concrete
60
+ * identifiers call this from `create`.
66
61
  */
67
62
  public static isValid(candidate: string): boolean {
68
63
  return UUID_PATTERN.test(candidate);
@@ -1,5 +1,6 @@
1
1
  import { PluginManager } from "../plugins/index.js";
2
2
  import { type DeepReadonly, makeImmutable } from "../shared/immutable.js";
3
+ import { stableStringify } from "../shared/stable-stringify.js";
3
4
  import { type ContractVersion, version } from "../versioning/version.js";
4
5
 
5
6
  /**
@@ -28,6 +29,12 @@ type ValueObjectPluginContract = {
28
29
  * any risk of a consumer mutating shared state. Subclasses seal the instance
29
30
  * itself with {@link finalize} once their own fields are set.
30
31
  *
32
+ * One exception: nested `Date` instances are cloned but NOT frozen —
33
+ * `Object.freeze` does not block `setTime`/`setFullYear`, so a consumer of
34
+ * `value` can still mutate an inner `Date` in place. Store instants as ISO
35
+ * strings or epoch timestamps in the wrapped state (converting to `Date` only
36
+ * in an accessor) when that guarantee matters.
37
+ *
31
38
  * @typeParam T - The shape of the wrapped data.
32
39
  * @typeParam P - The primitive form produced by {@link toPrimitive} / {@link toJSON}.
33
40
  */
@@ -40,6 +47,13 @@ abstract class ValueObject<T, P> {
40
47
  * default — when nothing is registered, {@link equals} falls back to a
41
48
  * structural comparison of the wrapped `value`. Hosts register a plugin
42
49
  * (e.g. `lodash.isEqual`) once at startup to customise equality globally.
50
+ *
51
+ * "Globally" is literal: this static registry is process-wide and shared
52
+ * by **every** `ValueObject` subclass, including the value objects the kit
53
+ * itself ships (RBAC's `Permission`, `Scope`, …). A registered plugin
54
+ * therefore redefines equality for those too — it must honour generic
55
+ * value-object semantics, not the quirks of one host type. For a
56
+ * type-specific comparison, override `equals` on that subclass instead.
43
57
  */
44
58
  public static readonly plugins =
45
59
  new PluginManager<ValueObjectPluginContract>();
@@ -61,7 +75,7 @@ abstract class ValueObject<T, P> {
61
75
  * `@version` decorator — used to detect state persisted under an older shape.
62
76
  */
63
77
  public get contractVersion(): ContractVersion {
64
- return ValueObject.CONTRACT_VERSION;
78
+ return (this.constructor as typeof ValueObject).CONTRACT_VERSION;
65
79
  }
66
80
 
67
81
  /**
@@ -93,13 +107,25 @@ abstract class ValueObject<T, P> {
93
107
  * same data are considered equal.
94
108
  *
95
109
  * Delegates to the registered equality {@link plugins}; with no plugin
96
- * registered it falls back to comparing the serialized wrapped `value`.
110
+ * registered it falls back to comparing the serialized wrapped `value`,
111
+ * with object keys sorted so insertion order (e.g. code-built vs
112
+ * JSON-rehydrated) never affects the result.
97
113
  * Subclasses may still override for a faster or domain-specific comparison.
114
+ *
115
+ * The fallback also requires both sides to be the same concrete class, so
116
+ * an `OrderId` never equals a `CustomerId` that wraps the same string —
117
+ * `other: this` only guards at compile time, and a cast would otherwise
118
+ * slip through. And because the fallback serializes via `stableStringify`,
119
+ * it inherits `JSON.stringify` semantics: `undefined` properties are
120
+ * dropped (`{a: 1, b: undefined}` equals `{a: 1}`) and `Map`/`Set` collapse
121
+ * to `{}` — see the caveats in `shared/stable-stringify.ts`. Override
122
+ * `equals` when the wrapped value relies on such shapes.
98
123
  */
99
124
  public equals(other: this): boolean {
100
125
  return ValueObject.plugins.invoke("equals", [this, other], {
101
126
  fallback: (a, b) =>
102
- JSON.stringify(a.value) === JSON.stringify(b.value),
127
+ a.constructor === b.constructor &&
128
+ stableStringify(a.value) === stableStringify(b.value),
103
129
  });
104
130
  }
105
131
 
@@ -1,6 +1,7 @@
1
1
  import { AppError } from "./app-error.js";
2
2
  import { ErrorCodes } from "./error-codes.js";
3
3
  import type { AppErrorOptions, ErrorSeverity } from "./types.js";
4
+ import { compactMetadata, pickAppErrorOptions } from "./utils/index.js";
4
5
 
5
6
  // ─────────────────────────────────────────────────────────────────────────────
6
7
  // Types
@@ -85,7 +86,7 @@ class AuthenticationError extends AppError {
85
86
  code: ErrorCodes.authentication.missingToken,
86
87
  reason: "missing_token",
87
88
  metadata: compactMetadata(options),
88
- ...extractAppErrorOptions(options),
89
+ ...pickAppErrorOptions(options),
89
90
  });
90
91
  }
91
92
 
@@ -103,7 +104,7 @@ class AuthenticationError extends AppError {
103
104
  correlationId: options?.correlationId,
104
105
  requestId: options?.requestId,
105
106
  }),
106
- ...extractAppErrorOptions(options),
107
+ ...pickAppErrorOptions(options),
107
108
  });
108
109
  }
109
110
 
@@ -115,7 +116,7 @@ class AuthenticationError extends AppError {
115
116
  code: ErrorCodes.authentication.expiredToken,
116
117
  reason: "expired_token",
117
118
  metadata: compactMetadata(options),
118
- ...extractAppErrorOptions(options),
119
+ ...pickAppErrorOptions(options),
119
120
  });
120
121
  }
121
122
 
@@ -135,37 +136,10 @@ class AuthenticationError extends AppError {
135
136
  code: ErrorCodes.authentication.invalidCredentials,
136
137
  reason: "invalid_credentials",
137
138
  metadata: compactMetadata(options),
138
- ...extractAppErrorOptions(options),
139
+ ...pickAppErrorOptions(options),
139
140
  });
140
141
  }
141
142
  }
142
143
 
143
- // ─────────────────────────────────────────────────────────────────────────────
144
- // Helpers
145
- // ─────────────────────────────────────────────────────────────────────────────
146
-
147
- function extractAppErrorOptions(options?: Partial<AppErrorOptions>) {
148
- return {
149
- cause: options?.cause,
150
- type: options?.type,
151
- severity: options?.severity,
152
- correlationId: options?.correlationId,
153
- requestId: options?.requestId,
154
- commandId: options?.commandId,
155
- createdAtIso: options?.createdAtIso,
156
- publicMessage: options?.publicMessage,
157
- };
158
- }
159
-
160
- function compactMetadata<T extends Record<string, unknown>>(
161
- input?: T,
162
- ): T | undefined {
163
- if (!input) return undefined;
164
-
165
- return Object.fromEntries(
166
- Object.entries(input).filter(([, value]) => value !== undefined),
167
- ) as T;
168
- }
169
-
170
144
  export { AuthenticationError };
171
145
  export type { AuthenticationErrorMetadata, AuthenticationErrorReason };
@@ -1,6 +1,7 @@
1
1
  import { AppError } from "./app-error.js";
2
2
  import { ErrorCodes } from "./error-codes.js";
3
3
  import type { AppErrorOptions, ErrorSeverity } from "./types.js";
4
+ import { pickAppErrorOptions } from "./utils/index.js";
4
5
 
5
6
  // ─────────────────────────────────────────────────────────────────────────────
6
7
  // Types
@@ -30,8 +31,12 @@ type AuthorizationErrorMetadata = {
30
31
  /** Resource identification (without leaking the full payload) */
31
32
  resource?: { type: string; id?: string };
32
33
 
33
- /** Optional: actor (do not include sensitive data) */
34
- actor?: { userId: string; role?: string };
34
+ /**
35
+ * Optional: the acting principal (do not include sensitive data). `actorId`
36
+ * is the raw `RequestedBy` value — a user UUID *or* a `"system:<job>"`
37
+ * identity — so it must not be assumed to be a human user.
38
+ */
39
+ actor?: { actorId: string; role?: string };
35
40
 
36
41
  /** Optional: expected requirement */
37
42
  required?: AuthorizationRequirement;
@@ -125,7 +130,7 @@ class AuthorizationError extends AppError {
125
130
  code: ErrorCodes.authorization.forbidden,
126
131
  reason: "forbidden",
127
132
  metadata: { reason: "forbidden", ...extractMetadataOnly(input) },
128
- ...extractAppErrorOptions(input),
133
+ ...pickAppErrorOptions(input),
129
134
  });
130
135
  }
131
136
 
@@ -154,7 +159,7 @@ class AuthorizationError extends AppError {
154
159
  ...extractMetadataOnly(input),
155
160
  decision: "deny",
156
161
  },
157
- ...extractAppErrorOptions(input),
162
+ ...pickAppErrorOptions(input),
158
163
  });
159
164
  }
160
165
 
@@ -180,7 +185,7 @@ class AuthorizationError extends AppError {
180
185
  reason: "missing_role",
181
186
  ...extractMetadataOnly(input),
182
187
  },
183
- ...extractAppErrorOptions(input),
188
+ ...pickAppErrorOptions(input),
184
189
  });
185
190
  }
186
191
 
@@ -204,7 +209,7 @@ class AuthorizationError extends AppError {
204
209
  reason: "missing_capability",
205
210
  ...extractMetadataOnly(input),
206
211
  },
207
- ...extractAppErrorOptions(input),
212
+ ...pickAppErrorOptions(input),
208
213
  });
209
214
  }
210
215
 
@@ -226,7 +231,7 @@ class AuthorizationError extends AppError {
226
231
  code: ErrorCodes.authorization.outOfScope,
227
232
  reason: "out_of_scope",
228
233
  metadata: { reason: "out_of_scope", ...extractMetadataOnly(input) },
229
- ...extractAppErrorOptions(input),
234
+ ...pickAppErrorOptions(input),
230
235
  });
231
236
  }
232
237
  }
@@ -235,19 +240,6 @@ class AuthorizationError extends AppError {
235
240
  // Helpers
236
241
  // ─────────────────────────────────────────────────────────────────────────────
237
242
 
238
- function extractAppErrorOptions(options?: Partial<AppErrorOptions>) {
239
- return {
240
- cause: options?.cause,
241
- type: options?.type,
242
- severity: options?.severity,
243
- correlationId: options?.correlationId,
244
- requestId: options?.requestId,
245
- commandId: options?.commandId,
246
- createdAtIso: options?.createdAtIso,
247
- publicMessage: options?.publicMessage,
248
- };
249
- }
250
-
251
243
  function extractMetadataOnly(
252
244
  input: AuthorizationErrorFactoryOptions,
253
245
  ): Omit<AuthorizationErrorMetadata, "reason"> {
@@ -14,14 +14,17 @@ class BusinessRuleViolationError extends AppError {
14
14
  options?: AppErrorOptions,
15
15
  ) {
16
16
  const baseMetadata = detail ? { rule, detail } : { rule };
17
+ // Reserved keys (`rule`/`detail`) spread last so caller metadata cannot
18
+ // clobber them.
17
19
  const mergedMetadata = options?.metadata
18
- ? { ...baseMetadata, ...options.metadata }
20
+ ? { ...options.metadata, ...baseMetadata }
19
21
  : baseMetadata;
20
22
 
21
23
  super(message, ErrorCodes.businessRuleViolation, {
22
24
  ...options,
23
25
  metadata: mergedMetadata,
24
26
  });
27
+ Object.freeze(this);
25
28
  }
26
29
  }
27
30
 
@@ -1,4 +1,5 @@
1
- import { payloadHash, sha256Hex, stableStringify } from "../shared/hashing.js";
1
+ import { payloadHash, sha256Hex } from "../shared/hashing.js";
2
+ import { stableStringify } from "../shared/stable-stringify.js";
2
3
 
3
4
  import { AppError } from "./app-error.js";
4
5
  import { ErrorCodes } from "./error-codes.js";
@@ -359,7 +360,9 @@ function resolveKeyIdentity(
359
360
  (key ? (key.length <= 8 ? key : key.slice(0, 8)) : undefined);
360
361
 
361
362
  return {
362
- keyHash: keyHash,
363
+ // Derive the hash from the raw key when the caller didn't supply one —
364
+ // cheap traceability without leaking the key itself.
365
+ keyHash: keyHash ?? (key ? sha256Hex(key) : undefined),
363
366
  keyPreview: preview,
364
367
  };
365
368
  }
@@ -74,6 +74,8 @@ export type {
74
74
  SerializationFailureCategory,
75
75
  } from "./serialization-error.js";
76
76
  export {
77
+ // `DeserializationError` is a readability alias for the inbound direction
78
+ // (deserialization == SerializationInError); both names are public API.
77
79
  SerializationInError as DeserializationError,
78
80
  safePreview,
79
81
  SerializationCodes,
@@ -83,6 +85,8 @@ export {
83
85
  SerializationOutError,
84
86
  } from "./serialization-error.js";
85
87
  export type {
88
+ // `ExpiredError*` aliases are kept for callers that reach for the
89
+ // expiry-specific name; they are the same shapes as the generic Temporal*.
86
90
  TemporalErrorMetadata as ExpiredErrorMetadata,
87
91
  TemporalPrecision as ExpiredErrorPrecision,
88
92
  TemporalErrorMetadata,
@@ -1,6 +1,7 @@
1
1
  import type { AppErrorOptions, ErrorSeverity } from "./types.js";
2
2
  import { AppError } from "./app-error.js";
3
3
  import { ErrorCodes } from "./error-codes.js";
4
+ import { compactMetadata, pickAppErrorOptions } from "./utils/index.js";
4
5
 
5
6
  type IntegrationErrorReason =
6
7
  | "timeout"
@@ -78,7 +79,7 @@ class IntegrationError extends AppError {
78
79
  code: ErrorCodes.integration.timeout,
79
80
  reason: "timeout",
80
81
  metadata: buildMetadata({ reason: "timeout", ...options }),
81
- ...pickAppErrorFields(options),
82
+ ...pickAppErrorOptions(options),
82
83
  });
83
84
  }
84
85
 
@@ -94,7 +95,7 @@ class IntegrationError extends AppError {
94
95
  code: ErrorCodes.integration.unreachable,
95
96
  reason: "unreachable",
96
97
  metadata: buildMetadata({ reason: "unreachable", ...options }),
97
- ...pickAppErrorFields(options),
98
+ ...pickAppErrorOptions(options),
98
99
  });
99
100
  }
100
101
 
@@ -111,7 +112,7 @@ class IntegrationError extends AppError {
111
112
  code: ErrorCodes.integration.badResponse,
112
113
  reason: "bad_response",
113
114
  metadata: buildMetadata({ reason: "bad_response", ...options }),
114
- ...pickAppErrorFields(options),
115
+ ...pickAppErrorOptions(options),
115
116
  });
116
117
  }
117
118
  }
@@ -157,26 +158,5 @@ function buildMetadata(
157
158
  }) as IntegrationErrorMetadata;
158
159
  }
159
160
 
160
- function pickAppErrorFields(
161
- options: IntegrationErrorBaseOptions,
162
- ): IntegrationErrorAppOptions {
163
- return {
164
- cause: options.cause,
165
- type: options.type,
166
- severity: options.severity,
167
- correlationId: options.correlationId,
168
- requestId: options.requestId,
169
- commandId: options.commandId,
170
- createdAtIso: options.createdAtIso,
171
- publicMessage: options.publicMessage,
172
- };
173
- }
174
-
175
- function compactMetadata<T extends Record<string, unknown>>(input: T): T {
176
- return Object.fromEntries(
177
- Object.entries(input).filter(([, value]) => value !== undefined),
178
- ) as T;
179
- }
180
-
181
161
  export { IntegrationError };
182
162
  export type { IntegrationErrorReason, IntegrationErrorMetadata };
@@ -20,6 +20,7 @@ class LegacyIncompatibleError extends AppError {
20
20
  ...options,
21
21
  metadata: mergedMetadata,
22
22
  });
23
+ Object.freeze(this);
23
24
  }
24
25
  }
25
26
 
@@ -27,14 +27,17 @@ class NotFoundError extends AppError {
27
27
  options?: AppErrorOptions,
28
28
  ) {
29
29
  const baseMetadata = criteria ? { resource, criteria } : { resource };
30
+ // Reserved keys (`resource`/`criteria`) spread last so caller metadata
31
+ // can add to but never clobber them — as the docs promise.
30
32
  const mergedMetadata = options?.metadata
31
- ? { ...baseMetadata, ...options.metadata }
33
+ ? { ...options.metadata, ...baseMetadata }
32
34
  : baseMetadata;
33
35
 
34
36
  super(`${resource} not found`, ErrorCodes.notFound, {
35
37
  ...options,
36
38
  metadata: mergedMetadata,
37
39
  });
40
+ Object.freeze(this);
38
41
  }
39
42
  }
40
43
 
@@ -1,6 +1,7 @@
1
1
  import { AppError } from "./app-error.js";
2
2
  import { ErrorCodes } from "./error-codes.js";
3
3
  import type { AppErrorOptions } from "./types.js";
4
+ import { pickAppErrorOptions, stripAppErrorOptions } from "./utils/index.js";
4
5
 
5
6
  type TemporalKind = "expired" | "not_yet_valid";
6
7
 
@@ -66,19 +67,12 @@ class ExpiredError extends TemporalError {
66
67
  code: ErrorCodes.temporal.expired,
67
68
  kind: "expired",
68
69
  metadata: {
69
- ...input,
70
+ ...stripAppErrorOptions(input),
70
71
  hint:
71
72
  input.hint ??
72
73
  "The window has expired. Request a new link or try again within the valid period.",
73
74
  },
74
- cause: input.cause,
75
- type: input.type,
76
- severity: input.severity,
77
- correlationId: input.correlationId,
78
- requestId: input.requestId,
79
- commandId: input.commandId,
80
- createdAtIso: input.createdAtIso,
81
- publicMessage: input.publicMessage,
75
+ ...pickAppErrorOptions(input),
82
76
  });
83
77
  }
84
78
  }
@@ -94,23 +88,31 @@ class NotYetValidError extends TemporalError {
94
88
  code: ErrorCodes.temporal.notYetValid,
95
89
  kind: "not_yet_valid",
96
90
  metadata: {
97
- ...input,
91
+ ...stripAppErrorOptions(input),
98
92
  hint:
99
93
  input.hint ??
100
94
  "Not yet within the valid period. Try again later.",
101
95
  },
102
- cause: input.cause,
103
- type: input.type,
104
- severity: input.severity,
105
- correlationId: input.correlationId,
106
- requestId: input.requestId,
107
- commandId: input.commandId,
108
- createdAtIso: input.createdAtIso,
109
- publicMessage: input.publicMessage,
96
+ ...pickAppErrorOptions(input),
110
97
  });
111
98
  }
112
99
  }
113
100
 
101
+ /**
102
+ * Evaluates whether `evaluatedAtIso` falls inside the [`validFromIso`,
103
+ * `validUntilIso`] window.
104
+ *
105
+ * Fails **closed**: a boundary that is present but unparseable is treated as if
106
+ * the window were closed on that side (a bad `validUntilIso` → expired, a bad
107
+ * `validFromIso` → not-yet-valid), never silently ignored — these checks guard
108
+ * access (invites, links, tokens), so a typo in an expiry must not disable
109
+ * expiry. A `null`/absent boundary means "no bound on that side", which is
110
+ * distinct from a malformed one. Returns `null` only when `evaluatedAtIso`
111
+ * itself is unparseable (nothing can be decided).
112
+ *
113
+ * For an inverted window (`validFrom` > `validUntil`) `expired` wins, since the
114
+ * upper-bound check short-circuits.
115
+ */
114
116
  function evaluateTemporalWindow(input: {
115
117
  validFromIso?: string | null;
116
118
  validUntilIso?: string | null;
@@ -123,14 +125,14 @@ function evaluateTemporalWindow(input: {
123
125
 
124
126
  if (input.validFromIso) {
125
127
  const fromMs = Date.parse(input.validFromIso);
126
- if (!Number.isNaN(fromMs) && evaluatedMs < fromMs) {
128
+ if (Number.isNaN(fromMs) || evaluatedMs < fromMs) {
127
129
  notYetValid = true;
128
130
  }
129
131
  }
130
132
 
131
133
  if (input.validUntilIso) {
132
134
  const untilMs = Date.parse(input.validUntilIso);
133
- if (!Number.isNaN(untilMs) && evaluatedMs > untilMs) {
135
+ if (Number.isNaN(untilMs) || evaluatedMs > untilMs) {
134
136
  return { expired: true, notYetValid: false };
135
137
  }
136
138
  }
@@ -12,10 +12,14 @@ class UnexpectedError extends AppError {
12
12
  cause?: unknown,
13
13
  options?: Omit<AppErrorOptions, "cause">,
14
14
  ) {
15
+ // The error you most want alerted on defaults to "critical" — a caller
16
+ // can still lower it via options.severity.
15
17
  super(message, ErrorCodes.unexpected, {
16
- cause,
18
+ severity: "critical",
17
19
  ...options,
20
+ cause,
18
21
  });
22
+ Object.freeze(this);
19
23
  }
20
24
  }
21
25
 
@@ -0,0 +1,70 @@
1
+ // Shared helpers for the error factories: separating the AppError envelope
2
+ // options from the free-form metadata, and dropping undefined-valued keys.
3
+ // Previously each of authentication/authorization/integration carried its own
4
+ // near-identical copy of these.
5
+
6
+ import type { AppErrorOptions } from "../types.js";
7
+
8
+ /** The envelope fields AppError understands; everything else is metadata. */
9
+ const APP_ERROR_OPTION_KEYS = [
10
+ "cause",
11
+ "type",
12
+ "severity",
13
+ "correlationId",
14
+ "requestId",
15
+ "commandId",
16
+ "createdAtIso",
17
+ "publicMessage",
18
+ ] as const;
19
+
20
+ type AppErrorEnvelope = Pick<
21
+ AppErrorOptions,
22
+ (typeof APP_ERROR_OPTION_KEYS)[number]
23
+ >;
24
+
25
+ /**
26
+ * Picks the AppError envelope fields out of a flat factory input, so they can
27
+ * be forwarded to the constructor as options instead of leaking into metadata.
28
+ */
29
+ function pickAppErrorOptions(
30
+ input?: Partial<AppErrorOptions>,
31
+ ): AppErrorEnvelope {
32
+ return {
33
+ cause: input?.cause,
34
+ type: input?.type,
35
+ severity: input?.severity,
36
+ correlationId: input?.correlationId,
37
+ requestId: input?.requestId,
38
+ commandId: input?.commandId,
39
+ createdAtIso: input?.createdAtIso,
40
+ publicMessage: input?.publicMessage,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * The complement of {@link pickAppErrorOptions}: everything that is *not* an
46
+ * AppError envelope field, i.e. the caller-supplied metadata. Keeps the cause
47
+ * and the public message out of the internal data block.
48
+ */
49
+ function stripAppErrorOptions<T extends Record<string, unknown>>(
50
+ input: T,
51
+ ): Omit<T, (typeof APP_ERROR_OPTION_KEYS)[number]> {
52
+ const result = { ...input };
53
+ for (const key of APP_ERROR_OPTION_KEYS) {
54
+ delete result[key];
55
+ }
56
+ return result;
57
+ }
58
+
59
+ /** Drops keys whose value is `undefined` (mirrors how JSON.stringify treats them). */
60
+ function compactMetadata<T extends Record<string, unknown>>(
61
+ input?: T,
62
+ ): T | undefined {
63
+ if (!input) return undefined;
64
+
65
+ return Object.fromEntries(
66
+ Object.entries(input).filter(([, value]) => value !== undefined),
67
+ ) as T;
68
+ }
69
+
70
+ export { compactMetadata, pickAppErrorOptions, stripAppErrorOptions };
@@ -1,3 +1,8 @@
1
+ export {
2
+ compactMetadata,
3
+ pickAppErrorOptions,
4
+ stripAppErrorOptions,
5
+ } from "./factory-helpers.js";
1
6
  export {
2
7
  assertJsonSafeMetadata,
3
8
  CIRCULAR_REFERENCE_PLACEHOLDER,