@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
@@ -47,6 +47,13 @@ abstract class ValueObject<T, P> {
47
47
  * default — when nothing is registered, {@link equals} falls back to a
48
48
  * structural comparison of the wrapped `value`. Hosts register a plugin
49
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.
50
57
  */
51
58
  public static readonly plugins =
52
59
  new PluginManager<ValueObjectPluginContract>();
@@ -104,10 +111,20 @@ abstract class ValueObject<T, P> {
104
111
  * with object keys sorted so insertion order (e.g. code-built vs
105
112
  * JSON-rehydrated) never affects the result.
106
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.
107
123
  */
108
124
  public equals(other: this): boolean {
109
125
  return ValueObject.plugins.invoke("equals", [this, other], {
110
126
  fallback: (a, b) =>
127
+ a.constructor === b.constructor &&
111
128
  stableStringify(a.value) === stableStringify(b.value),
112
129
  });
113
130
  }
@@ -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,
@@ -75,7 +75,21 @@ function sanitizeValue(value: unknown, seen: WeakSet<object>): JsonSafeValue {
75
75
  const result: Record<string, JsonSafeValue> = {};
76
76
  for (const [key, val] of Object.entries(value)) {
77
77
  if (val === undefined) continue;
78
- result[key] = sanitizeValue(val, seen);
78
+ const sanitized = sanitizeValue(val, seen);
79
+ if (key === "__proto__") {
80
+ // Plain `result.__proto__ = x` would reparent `result` instead of
81
+ // creating an own data property, silently dropping the key. Define
82
+ // it explicitly so `__proto__` survives as data — matching how
83
+ // JSON.stringify/JSON.parse round-trip it.
84
+ Object.defineProperty(result, key, {
85
+ value: sanitized,
86
+ enumerable: true,
87
+ writable: true,
88
+ configurable: true,
89
+ });
90
+ } else {
91
+ result[key] = sanitized;
92
+ }
79
93
  }
80
94
  seen.delete(value);
81
95
  return result;
@@ -38,14 +38,17 @@ class ValidationError extends AppError {
38
38
  field: field.value,
39
39
  validationCode: code.value,
40
40
  };
41
+ // `field`/`validationCode` spread last so caller metadata is merged
42
+ // over them without ever overwriting them by accident.
41
43
  const mergedMetadata = options?.metadata
42
- ? { ...baseMetadata, ...options.metadata }
44
+ ? { ...options.metadata, ...baseMetadata }
43
45
  : baseMetadata;
44
46
 
45
47
  super(message, ErrorCodes.validation, {
46
48
  ...options,
47
49
  metadata: mergedMetadata,
48
50
  });
51
+ Object.freeze(this);
49
52
  }
50
53
  }
51
54
 
@@ -4,8 +4,9 @@ class BusinessRuleViolationException extends DomainException {
4
4
  constructor(
5
5
  public readonly rule: string,
6
6
  message: string,
7
+ options?: { cause?: unknown },
7
8
  ) {
8
- super(message);
9
+ super(message, options);
9
10
  }
10
11
  }
11
12
 
@@ -1,6 +1,14 @@
1
1
  abstract class DomainException extends Error {
2
- constructor(message: string) {
2
+ // ES2020's `Error` lib type does not declare `cause`; mirror the sibling
3
+ // `AppError` and carry it explicitly so a domain exception can wrap the
4
+ // lower-level failure that triggered it. Assigned rather than passed to
5
+ // `super` to stay independent of the ES2022 `Error(message, { cause })`
6
+ // overload the build target doesn't guarantee.
7
+ public readonly cause?: unknown;
8
+
9
+ constructor(message: string, options?: { cause?: unknown }) {
3
10
  super(message);
11
+ this.cause = options?.cause;
4
12
  Object.setPrototypeOf(this, new.target.prototype);
5
13
  this.name = new.target.name;
6
14
  }
@@ -4,8 +4,12 @@ class EntityNotFoundException extends DomainException {
4
4
  constructor(
5
5
  public readonly entityName: string,
6
6
  public readonly identifier: string,
7
+ options?: { cause?: unknown },
7
8
  ) {
8
- super(`${entityName} with identifier ${identifier} was not found.`);
9
+ super(
10
+ `${entityName} with identifier ${identifier} was not found.`,
11
+ options,
12
+ );
9
13
  }
10
14
  }
11
15
 
@@ -6,9 +6,12 @@ class InvalidStateTransitionException<
6
6
  constructor(
7
7
  public readonly from: TState,
8
8
  public readonly to: TState,
9
- message: string,
9
+ message?: string,
10
+ options?: { cause?: unknown },
10
11
  ) {
11
- super(message);
12
+ // Compose a sensible default from `from`/`to` when no message is given,
13
+ // mirroring how EntityNotFoundException builds its own message.
14
+ super(message ?? `Cannot transition from ${from} to ${to}.`, options);
12
15
  }
13
16
  }
14
17
 
@@ -1,8 +1,8 @@
1
1
  import { DomainException } from "./domain-exception.js";
2
2
 
3
3
  class InvariantViolationException extends DomainException {
4
- constructor(message: string) {
5
- super(message);
4
+ constructor(message: string, options?: { cause?: unknown }) {
5
+ super(message, options);
6
6
  }
7
7
  }
8
8
 
@@ -13,6 +13,20 @@ class ValidationCode {
13
13
  return new ValidationCode(value);
14
14
  }
15
15
 
16
+ public toString(): string {
17
+ return this.value;
18
+ }
19
+
20
+ public equals(other: ValidationCode): boolean {
21
+ return this.value === other.value;
22
+ }
23
+
24
+ // Serialize to the bare code string, so JSON.stringify of a violation
25
+ // yields "blank" rather than { "value": "blank" }. Mirrors ValidationField.
26
+ public toJSON(): string {
27
+ return this.value;
28
+ }
29
+
16
30
  // Common, domain-friendly codes
17
31
  public static readonly BLANK = new ValidationCode("blank");
18
32
  public static readonly REQUIRED = new ValidationCode("required");