@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
@@ -23,11 +23,7 @@ import { PolicyResolver } from "../resolver/index.js";
23
23
  import { Result } from "../../result/result.js";
24
24
  import { isValidDate } from "../../shared/temporal-guards.js";
25
25
 
26
- import type {
27
- PolicyEvent,
28
- PolicyReporter,
29
- } from "../../config/policy-reporter.js";
30
- import { SilentPolicyReporter } from "../../config/silent-policy-reporter.js";
26
+ import { type CoreConfig, coreConfig } from "../../config/index.js";
31
27
 
32
28
  import {
33
29
  PolicyEvaluationErrors,
@@ -42,6 +38,21 @@ import {
42
38
  * `contextVersion` pins which context schema applies, and `seed` carries the
43
39
  * raw facts the context builder expands. `decisionId` is the caller's
44
40
  * idempotency/audit handle, echoed back on the result.
41
+ *
42
+ * `contextVersion` is a caller assertion, not a proof: it filters which
43
+ * definitions are compatible, but nothing checks that the context actually
44
+ * built matches the declared version — supplying the right number is the
45
+ * caller's contract.
46
+ *
47
+ * Two `seed.fields` keys are reserved and change what "now" means for this
48
+ * evaluation:
49
+ * - `now` (a `Date`) pins the evaluation clock — it feeds `evaluatedAt`, the
50
+ * `asOf` derivation, and therefore which effective-dated definitions are in
51
+ * force. Leave it unset to use wall-clock time.
52
+ * - `asOf` (a `Date`) is consumed when the policy's `asOfSource` is
53
+ * `CALLER_PROVIDED` (see {@link PolicyAsOfResolver}).
54
+ * Whoever assembles `fields` from untrusted input owns the consequence: a
55
+ * caller who can set these keys can shift which policy version is selected.
45
56
  */
46
57
  export interface EvaluateInput {
47
58
  readonly decisionId: PolicyDecisionId;
@@ -52,14 +63,13 @@ export interface EvaluateInput {
52
63
  }
53
64
 
54
65
  /**
55
- * Cross-cutting knobs for a {@link PolicyService}: how the as-of instant is
56
- * derived ({@link asOf}) and where evaluation telemetry is sent
57
- * ({@link reporter}). Both are optional; the service defaults to a silent
58
- * reporter so telemetry is opt-in.
66
+ * Cross-cutting knobs for a {@link PolicyService}: today just how the as-of
67
+ * instant is derived ({@link asOf}). Telemetry is not configured here — it
68
+ * flows through the injected {@link CoreConfig}, the core's single
69
+ * observability seam, so a host wires reporting in one place.
59
70
  */
60
71
  export interface PolicyServiceOptions {
61
72
  readonly asOf?: DeriveAsOfOptions;
62
- readonly reporter?: PolicyReporter;
63
73
  }
64
74
 
65
75
  /**
@@ -75,6 +85,12 @@ export interface PolicyServiceParams {
75
85
  readonly resolver: PolicyResolver;
76
86
  readonly gateEngines: GateEngineRegistry;
77
87
  readonly computeRegistry: ComputeRegistry;
88
+ /**
89
+ * The observability seam telemetry is emitted through. Defaults to the
90
+ * shared {@link coreConfig} singleton; inject an isolated instance for
91
+ * tests or multi-tenant hosts.
92
+ */
93
+ readonly coreConfig?: CoreConfig;
78
94
  readonly options?: PolicyServiceOptions;
79
95
  }
80
96
 
@@ -89,8 +105,11 @@ export type { PolicyEvaluationError } from "./policy-evaluation-error.js";
89
105
  * The full record of one successful evaluation. Beyond the business
90
106
  * {@link decision}, it pins the exact definition that produced it — id, key,
91
107
  * version, payload hash, and engine version — together with the `asOf` instant
92
- * the policy was selected for and the wall-clock `evaluatedAt`. That provenance
93
- * is what makes a decision reproducible and auditable after the fact.
108
+ * the policy was selected for and `evaluatedAt`, the instant evaluation ran at.
109
+ * `evaluatedAt` is wall-clock time unless the caller pinned it via
110
+ * `seed.fields.now` (see {@link EvaluateInput}), in which case it echoes that.
111
+ * That provenance is what makes a decision reproducible and auditable after the
112
+ * fact.
94
113
  */
95
114
  export interface PolicyEvaluationResult {
96
115
  readonly decisionId: PolicyDecisionId;
@@ -130,7 +149,7 @@ interface ResolvedPolicyCandidate {
130
149
  * evaluation — which keeps observability strictly side-band.
131
150
  */
132
151
  export class PolicyService {
133
- readonly #reporter: PolicyReporter;
152
+ private readonly coreConfig: CoreConfig;
134
153
  private readonly catalog: PolicyCatalog;
135
154
  private readonly contextBuilder: PolicyContextBuilder;
136
155
  private readonly defRepo: PolicyDefinitionRepository;
@@ -147,15 +166,7 @@ export class PolicyService {
147
166
  this.gateEngines = params.gateEngines;
148
167
  this.computeRegistry = params.computeRegistry;
149
168
  this.options = params.options ?? {};
150
- this.#reporter = this.options.reporter ?? new SilentPolicyReporter();
151
- }
152
-
153
- #reportSafely(event: PolicyEvent): void {
154
- try {
155
- this.#reporter.report(event);
156
- } catch {
157
- // Falhas de telemetria nao podem interromper a avaliacao de politicas.
158
- }
169
+ this.coreConfig = params.coreConfig ?? coreConfig;
159
170
  }
160
171
 
161
172
  private resolveEvaluationNow(seed: ContextSeed): Result<Date, string> {
@@ -192,10 +203,10 @@ export class PolicyService {
192
203
  async evaluate(
193
204
  input: EvaluateInput,
194
205
  ): Promise<Result<PolicyEvaluationResult, PolicyEvaluationError>> {
195
- const result = await this.#evaluate(input);
206
+ const result = await this.runEvaluation(input);
196
207
  if (result.isErr()) {
197
208
  const error = result.errorOrNull()!;
198
- this.#reportSafely({
209
+ this.coreConfig.reportSafely({
199
210
  kind: "policy-evaluation-failed",
200
211
  level: "error",
201
212
  policyKey: "policyKey" in error ? error.policyKey : undefined,
@@ -206,7 +217,7 @@ export class PolicyService {
206
217
  });
207
218
  } else {
208
219
  const ok = result.getOrNull()!;
209
- this.#reportSafely({
220
+ this.coreConfig.reportSafely({
210
221
  kind: "policy-evaluation-completed",
211
222
  level: "info",
212
223
  policyKey: ok.ref.policyKey,
@@ -218,7 +229,7 @@ export class PolicyService {
218
229
  return result;
219
230
  }
220
231
 
221
- async #evaluate(
232
+ private async runEvaluation(
222
233
  input: EvaluateInput,
223
234
  ): Promise<Result<PolicyEvaluationResult, PolicyEvaluationError>> {
224
235
  const seedResult = ContextSeedValidator.validate(input.seed);
@@ -233,7 +244,7 @@ export class PolicyService {
233
244
  }
234
245
  const seed = seedResult.getOrNull()!;
235
246
 
236
- const catalogFamilyResult = this.#resolveCatalogFamily(input.policyKey);
247
+ const catalogFamilyResult = this.resolveCatalogFamily(input.policyKey);
237
248
  if (catalogFamilyResult.isErr()) {
238
249
  return Result.err(catalogFamilyResult.errorOrNull()!);
239
250
  }
@@ -269,7 +280,7 @@ export class PolicyService {
269
280
  }
270
281
  const asOf = asOfResult.getOrNull()!;
271
282
 
272
- const definitionResult = this.#findCandidates(
283
+ const definitionResult = this.findCandidates(
273
284
  policyKey,
274
285
  familyEntry.kind,
275
286
  asOf,
@@ -283,7 +294,7 @@ export class PolicyService {
283
294
  catalogEntry: versionedCatalogEntry,
284
295
  } = definitionResult.getOrNull()!;
285
296
 
286
- this.#reportSafely({
297
+ this.coreConfig.reportSafely({
287
298
  kind: "policy-resolution",
288
299
  level: "info",
289
300
  policyKey,
@@ -311,7 +322,7 @@ export class PolicyService {
311
322
  }
312
323
  const context = ctxResult.getOrNull()!;
313
324
 
314
- const decisionResult = this.#executeEngine(
325
+ const decisionResult = this.executeEngine(
315
326
  policyKey,
316
327
  policyDefinition,
317
328
  context,
@@ -347,7 +358,7 @@ export class PolicyService {
347
358
  return Result.ok(result);
348
359
  }
349
360
 
350
- #resolveCatalogVariant(
361
+ private resolveCatalogVariant(
351
362
  definition: PolicyDefinition,
352
363
  ): Result<PolicyCatalogEntry, string> {
353
364
  return this.catalog.getVersioned({
@@ -362,7 +373,7 @@ export class PolicyService {
362
373
  });
363
374
  }
364
375
 
365
- #resolveCatalogFamily(
376
+ private resolveCatalogFamily(
366
377
  rawKey: string,
367
378
  ): Result<
368
379
  { key: string; family: readonly PolicyCatalogEntry[] },
@@ -395,7 +406,7 @@ export class PolicyService {
395
406
  });
396
407
  }
397
408
 
398
- #findCandidates(
409
+ private findCandidates(
399
410
  policyKey: string,
400
411
  policyKind: PolicyCatalogEntry["kind"],
401
412
  asOf: Date,
@@ -420,7 +431,7 @@ export class PolicyService {
420
431
  >();
421
432
 
422
433
  for (const candidate of candidates) {
423
- const compatibilityResult = this.#resolveCatalogVariant(candidate);
434
+ const compatibilityResult = this.resolveCatalogVariant(candidate);
424
435
  if (compatibilityResult.isErr()) {
425
436
  incompatibleVariantErrorsByDefinitionId.set(
426
437
  candidate.id,
@@ -503,7 +514,7 @@ export class PolicyService {
503
514
  return Result.ok(resolvedCandidate);
504
515
  }
505
516
 
506
- #executeEngine(
517
+ private executeEngine(
507
518
  policyKey: string,
508
519
  definition: PolicyDefinition,
509
520
  context: PolicyContext,
@@ -1,5 +1,5 @@
1
- import { sha256Hex, stableStringify } from "../../shared/hashing.js";
2
- import { isValidDate } from "../../shared/temporal-guards.js";
1
+ import { sha256Hex } from "../../shared/hashing.js";
2
+ import { canonicalStringify } from "../../shared/stable-stringify.js";
3
3
 
4
4
  /**
5
5
  * Produces a deterministic SHA-256 hex digest of a canonical JSON representation.
@@ -7,6 +7,8 @@ import { isValidDate } from "../../shared/temporal-guards.js";
7
7
  * Array item order is preserved on purpose; callers that treat arrays as sets
8
8
  * must normalize or sort them before hashing.
9
9
  *
10
+ * The strict canonical form is owned by `shared/stable-stringify`
11
+ * ({@link canonicalStringify}); this class just composes it with SHA-256.
10
12
  * Values that JSON.stringify would silently drop or coerce (undefined,
11
13
  * non-finite numbers, functions, symbols, bigint) are rejected up-front to
12
14
  * prevent semantically different payloads from collapsing to the same hash.
@@ -16,74 +18,8 @@ export class PolicyHashing {
16
18
  return sha256Hex(input);
17
19
  }
18
20
 
19
- private static assertHashable(
20
- value: unknown,
21
- path: string,
22
- seen: WeakSet<object>,
23
- ): void {
24
- if (value === null) {
25
- return;
26
- }
27
-
28
- const type = typeof value;
29
-
30
- if (type === "undefined") {
31
- throw new TypeError(
32
- `canonicalJson does not accept undefined values (at ${path})`,
33
- );
34
- }
35
-
36
- if (type === "number" && !Number.isFinite(value)) {
37
- throw new TypeError(
38
- `canonicalJson does not accept non-finite numbers (at ${path}, value: ${String(value)})`,
39
- );
40
- }
41
-
42
- if (type === "bigint" || type === "function" || type === "symbol") {
43
- throw new TypeError(
44
- `canonicalJson does not accept ${type} values (at ${path})`,
45
- );
46
- }
47
-
48
- if (type !== "object") {
49
- return;
50
- }
51
-
52
- if (value instanceof Date) {
53
- if (!isValidDate(value)) {
54
- throw new TypeError(
55
- `canonicalJson does not accept Invalid Date (at ${path})`,
56
- );
57
- }
58
- return;
59
- }
60
-
61
- if (seen.has(value as object)) {
62
- throw new TypeError(
63
- `canonicalJson does not accept circular references (at ${path})`,
64
- );
65
- }
66
- seen.add(value as object);
67
-
68
- if (Array.isArray(value)) {
69
- value.forEach((item, index) => {
70
- PolicyHashing.assertHashable(item, `${path}[${index}]`, seen);
71
- });
72
- } else {
73
- for (const [key, nested] of Object.entries(
74
- value as Record<string, unknown>,
75
- )) {
76
- PolicyHashing.assertHashable(nested, `${path}.${key}`, seen);
77
- }
78
- }
79
-
80
- seen.delete(value as object);
81
- }
82
-
83
21
  static canonicalJson(value: unknown): string {
84
- PolicyHashing.assertHashable(value, "$", new WeakSet<object>());
85
-
86
- return stableStringify(value);
22
+ return canonicalStringify(value);
87
23
  }
88
24
 
89
25
  static computePayloadHash(
@@ -1,4 +1,4 @@
1
1
  // Re-export the core Result class.
2
2
  // The policies module uses Result<T, string> as its standard Result type.
3
3
 
4
- export { Err, Ok, Result } from "../../result/result.js";
4
+ export { Err, Ok, Result } from "../../result/index.js";
@@ -22,10 +22,20 @@ interface AccessRequest {
22
22
  */
23
23
  readonly required: Permission;
24
24
 
25
- /** The target resource, identified without leaking its payload. */
25
+ /**
26
+ * The target resource, identified without leaking its payload. Carried for
27
+ * audit metadata only — the RBAC decisor does **not** check it. Per-resource
28
+ * ownership (this actor may cancel *this* order) is an ABAC/policy concern,
29
+ * not a role/permission one.
30
+ */
26
31
  readonly resource?: { readonly type: string; readonly id?: string };
27
32
 
28
- /** The scope the resource lives in, e.g. `Scope.of("school:123")`. */
33
+ /**
34
+ * The scope the resource lives in, e.g. `Scope.of("school:123")`. Asking in
35
+ * the global scope (`Scope.global()`) means "may I do this *everywhere*?" and
36
+ * so requires a globally-scoped grant — it is **not** "in any scope I happen
37
+ * to hold". Ask about a concrete scope to check a bounded grant.
38
+ */
29
39
  readonly scope: Scope;
30
40
  }
31
41
 
@@ -36,6 +36,13 @@ class RbacAuthorizer {
36
36
  * 3. grants the permission but not in scope → `out_of_scope`;
37
37
  * 4. otherwise → ALLOW (`Result.ok`).
38
38
  *
39
+ * ALLOW is intentionally silent: `Result.ok(undefined)` carries no trace of
40
+ * *which* grant or role decided it. Denials are rich (see the metadata
41
+ * below) because they need to explain themselves; an allow does not. When an
42
+ * ERP needs a "who allowed this, and why" trail, that is the adapter's job —
43
+ * reconstruct it from the grants it loaded, or gate on `PermissionSet` before
44
+ * calling. Keeping the decisor's allow-path pure is the deliberate trade.
45
+ *
39
46
  * @throws {@link InvalidValueException} when `request.required` is a wildcard
40
47
  * permission (only *granted* permissions may wildcard, never the *required*
41
48
  * one).
@@ -59,7 +66,7 @@ class RbacAuthorizer {
59
66
  const metadata = {
60
67
  action: request.action,
61
68
  resource: request.resource,
62
- actor: { userId: request.actor.raw },
69
+ actor: { actorId: request.actor.raw },
63
70
  required,
64
71
  };
65
72
 
@@ -83,6 +83,26 @@ class Role extends ValueObject<RoleProps, RoleProps> {
83
83
  return this._permissions;
84
84
  }
85
85
 
86
+ /**
87
+ * Two roles are equal when they share a name and the same *set* of
88
+ * permissions. Order-insensitive on purpose: a role is a bundle, not a
89
+ * sequence, so `Role.of("cashier", [read, cancel])` equals
90
+ * `Role.of("cashier", [cancel, read])`. Overrides the base
91
+ * {@link ValueObject.equals}, whose stable-stringify keeps array order and
92
+ * would call those two roles different. The stored order (and thus
93
+ * {@link permissions}/{@link toPrimitive}) is still first-seen; only the
94
+ * comparison sorts.
95
+ */
96
+ override equals(other: this): boolean {
97
+ if (this.value.name !== other.value.name) return false;
98
+ const mine = [...this.value.permissions].sort();
99
+ const theirs = [...other.value.permissions].sort();
100
+ return (
101
+ mine.length === theirs.length &&
102
+ mine.every((permission, index) => permission === theirs[index])
103
+ );
104
+ }
105
+
86
106
  /** Whether any permission in this role {@link Permission.implies | implies} `required`. */
87
107
  grants(required: Permission): boolean {
88
108
  return this._permissions.some((permission) =>
@@ -29,7 +29,12 @@ class Scope extends ValueObject<ScopeProps, string> {
29
29
  this.finalize();
30
30
  }
31
31
 
32
- /** Validates `"type:id"` or the global `"*"`, throwing on anything else. */
32
+ /**
33
+ * Validates `"type:id"` or the global `"*"`, throwing on anything else.
34
+ * `Scope.of("*")` is accepted and is equivalent to {@link Scope.global} —
35
+ * prefer `global()` at call-sites for intent, `of("*")` when rehydrating a
36
+ * stored string.
37
+ */
33
38
  static of(raw: string): Scope {
34
39
  if (typeof raw !== "string" || raw.trim().length === 0) {
35
40
  throw new InvalidValueException(
@@ -0,0 +1,5 @@
1
+ // Barrel for the result module — single source of truth for what it exports.
2
+ // Result = technical success/failure; Outcome = business decision.
3
+
4
+ export { Err, Ok, Result } from "./result.js";
5
+ export { type CommonOutcomeStatus, Outcome } from "./outcome.js";
@@ -1,5 +1,3 @@
1
- // core/domain/outcome.ts
2
-
3
1
  import { InvariantViolationException } from "../exceptions/invariant-violation-exception.js";
4
2
 
5
3
  /**
@@ -59,32 +57,32 @@ export class Outcome<S extends string, D = undefined> {
59
57
  data: D,
60
58
  reason?: string,
61
59
  ): Outcome<"APPROVED", D> {
62
- return new Outcome("APPROVED", data, reason, Object.freeze({}));
60
+ return Outcome.of("APPROVED", data, reason);
63
61
  }
64
62
 
65
63
  static rejected<D = undefined>(
66
64
  data: D,
67
65
  reason: string,
68
66
  ): Outcome<"REJECTED", D> {
69
- return new Outcome("REJECTED", data, reason, Object.freeze({}));
67
+ return Outcome.of("REJECTED", data, reason);
70
68
  }
71
69
 
72
70
  static noOp<D = undefined>(data: D, reason?: string): Outcome<"NO_OP", D> {
73
- return new Outcome("NO_OP", data, reason, Object.freeze({}));
71
+ return Outcome.of("NO_OP", data, reason);
74
72
  }
75
73
 
76
74
  static deferred<D = undefined>(
77
75
  data: D,
78
76
  reason: string,
79
77
  ): Outcome<"DEFERRED", D> {
80
- return new Outcome("DEFERRED", data, reason, Object.freeze({}));
78
+ return Outcome.of("DEFERRED", data, reason);
81
79
  }
82
80
 
83
81
  static requiresReview<D = undefined>(
84
82
  data: D,
85
83
  reason: string,
86
84
  ): Outcome<"REQUIRES_REVIEW", D> {
87
- return new Outcome("REQUIRES_REVIEW", data, reason, Object.freeze({}));
85
+ return Outcome.of("REQUIRES_REVIEW", data, reason);
88
86
  }
89
87
 
90
88
  // ─── Type narrowing ────────────────────────────────────────────────
@@ -41,6 +41,9 @@ export abstract class Result<T, E> {
41
41
 
42
42
  /**
43
43
  * Returns the value when this is success, or null when this is an error.
44
+ *
45
+ * Note: an `Ok(null)` also returns `null`, indistinguishable from an `Err`.
46
+ * Use `match`/`isOk` when that distinction matters.
44
47
  */
45
48
  getOrNull(): T | null {
46
49
  return this.match({
@@ -66,13 +69,15 @@ export abstract class Result<T, E> {
66
69
  return this.match({
67
70
  ok: (value) => value,
68
71
  err: (error) => {
69
- throw error instanceof Error ? error : new Error(String(error));
72
+ throw toError(error);
70
73
  },
71
74
  });
72
75
  }
73
76
 
74
77
  /**
75
78
  * Transforms the success value using the provided function. Keeps the error when this is a failure.
79
+ *
80
+ * Exceptions thrown by `transform` propagate; they are not captured into an `Err`.
76
81
  */
77
82
  map<R>(transform: (value: T) => R): Result<R, E> {
78
83
  return this.match<Result<R, E>>({
@@ -85,6 +90,8 @@ export abstract class Result<T, E> {
85
90
 
86
91
  /**
87
92
  * Transforms the error using the provided function. Keeps the value when this is a success.
93
+ *
94
+ * Exceptions thrown by `transform` propagate; they are not captured into an `Err`.
88
95
  */
89
96
  mapError<F>(transform: (error: E) => F): Result<T, F> {
90
97
  return this.match<Result<T, F>>({
@@ -96,6 +103,8 @@ export abstract class Result<T, E> {
96
103
 
97
104
  /**
98
105
  * Chains another operation that returns a Result when this result is a success.
106
+ *
107
+ * Exceptions thrown by `transform` propagate; they are not captured into an `Err`.
99
108
  */
100
109
  flatMap<R>(transform: (value: T) => Result<R, E>): Result<R, E> {
101
110
  return this.match<Result<R, E>>({
@@ -107,6 +116,9 @@ export abstract class Result<T, E> {
107
116
 
108
117
  /**
109
118
  * Success variant of Result.
119
+ *
120
+ * The instance is frozen, but the freeze is shallow: a contained mutable
121
+ * object is not frozen.
110
122
  */
111
123
  export class Ok<T> extends Result<T, never> {
112
124
  public readonly value: T;
@@ -154,3 +166,24 @@ export class Err<E> extends Result<never, E> {
154
166
  return handlers.err(this.error);
155
167
  }
156
168
  }
169
+
170
+ /**
171
+ * Coerces an arbitrary error value into an Error for `getOrThrow`, preserving
172
+ * the original structured payload as `cause` so nothing is lost when `E` is a
173
+ * plain object/DTO rather than an Error.
174
+ */
175
+ function toError(error: unknown): Error {
176
+ if (error instanceof Error) return error;
177
+ if (typeof error === "string") return new Error(error);
178
+ let message: string | undefined;
179
+ try {
180
+ message = JSON.stringify(error);
181
+ } catch {
182
+ // circular / BigInt / etc.
183
+ }
184
+ const wrapped = new Error(message ?? String(error));
185
+ // Attach the original payload as `cause` (assigned, not passed to the
186
+ // ES2022 constructor option, to stay within the ES2020 lib target).
187
+ (wrapped as Error & { cause?: unknown }).cause = error;
188
+ return wrapped;
189
+ }
@@ -1,67 +1,16 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
- const CIRCULAR_STRUCTURE_MESSAGE =
4
- "Cannot stably stringify a circular structure";
3
+ import { canonicalStringify } from "./stable-stringify.js";
5
4
 
6
5
  function sha256Hex(value: string): string {
7
6
  return createHash("sha256").update(value, "utf8").digest("hex");
8
7
  }
9
8
 
10
- // `seen` holds the current traversal branch so true cycles are rejected while
11
- // repeated (acyclic) references are still serialized matching JSON.stringify.
12
- function sortKeysDeep(value: unknown, seen: WeakSet<object>): unknown {
13
- if (Array.isArray(value)) {
14
- if (seen.has(value)) {
15
- throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
16
- }
17
- seen.add(value);
18
- const mapped = value.map((item) => sortKeysDeep(item, seen));
19
- seen.delete(value);
20
- return mapped;
21
- }
22
-
23
- if (value instanceof Date) {
24
- return value;
25
- }
26
-
27
- if (value && typeof value === "object") {
28
- if (seen.has(value)) {
29
- throw new TypeError(CIRCULAR_STRUCTURE_MESSAGE);
30
- }
31
- seen.add(value);
32
- const record = value as Record<string, unknown>;
33
- const ordered = Object.create(null) as Record<string, unknown>;
34
-
35
- for (const key of Object.keys(record).sort()) {
36
- ordered[key] = sortKeysDeep(record[key], seen);
37
- }
38
- seen.delete(value);
39
-
40
- return ordered;
41
- }
42
-
43
- return value;
44
- }
45
-
46
- /**
47
- * Deterministic JSON string with object keys sorted recursively, so key order
48
- * does not affect the output.
49
- *
50
- * Follows `JSON.stringify` semantics for the values it serializes: `undefined`,
51
- * functions and symbols are dropped, non-finite numbers become `null`, `bigint`
52
- * throws, and `Map`/`Set` serialize as `{}`. Distinct payloads can therefore
53
- * collapse to the same string — for collision-resistant hashing use
54
- * `PolicyHashing.canonicalJson` (policies/utils), which rejects those values
55
- * up front.
56
- *
57
- * @throws {TypeError} On circular references (mirroring `JSON.stringify`).
58
- */
59
- function stableStringify(value: unknown): string {
60
- return JSON.stringify(sortKeysDeep(value, new WeakSet<object>()));
61
- }
62
-
9
+ // Strict by design: hashes the collision-resistant canonical form, so payloads
10
+ // that differ only in values `JSON.stringify` would drop (undefined, NaN, …)
11
+ // produce distinct digests instead of silently colliding.
63
12
  function payloadHash(value: unknown): string {
64
- return sha256Hex(stableStringify(value));
13
+ return sha256Hex(canonicalStringify(value));
65
14
  }
66
15
 
67
- export { payloadHash, sha256Hex, stableStringify };
16
+ export { payloadHash, sha256Hex };
@@ -26,6 +26,12 @@ function deepFreezeInternal<T>(value: T, seen: WeakSet<object>): T {
26
26
  // property — freezing it is a no-op against mutation. `makeImmutable`
27
27
  // already hands us a `structuredClone` copy, so the caller's original Date
28
28
  // is never aliased; deep-freezing of nested Dates is deliberately skipped.
29
+ //
30
+ // The same no-op caveat applies to `Map`/`Set`: `Object.freeze` locks the
31
+ // wrapper's own properties but not `.set`/`.delete`/`.clear`, which mutate
32
+ // internal slots. A frozen Map is still mutable through its methods. We do
33
+ // not neuter those methods here (no value object wraps a Map today); if one
34
+ // ever does, its immutability guarantee would be false — handle it there.
29
35
  if (typeof value !== "object" || value === null || value instanceof Date) {
30
36
  return value;
31
37
  }
@@ -48,7 +54,17 @@ function deepFreezeInternal<T>(value: T, seen: WeakSet<object>): T {
48
54
  const objectValue = value as Record<PropertyKey, object | PrimitiveValue>;
49
55
 
50
56
  for (const propertyKey of Reflect.ownKeys(objectValue)) {
51
- deepFreezeInternal(objectValue[propertyKey], seen);
57
+ // Read through the descriptor rather than `objectValue[propertyKey]` so
58
+ // accessor properties are not invoked — a getter could have side effects
59
+ // or throw, and its computed result can't be frozen anyway. Only data
60
+ // properties hold a nested value worth recursing into.
61
+ const descriptor = Object.getOwnPropertyDescriptor(
62
+ objectValue,
63
+ propertyKey,
64
+ );
65
+ if (descriptor && "value" in descriptor) {
66
+ deepFreezeInternal(descriptor.value, seen);
67
+ }
52
68
  }
53
69
 
54
70
  return Object.freeze(value);
@@ -63,9 +79,11 @@ function makeImmutable<T>(value: T): DeepReadonly<T> {
63
79
  try {
64
80
  snapshot = structuredClone(value);
65
81
  } catch (error) {
66
- // `structuredClone` throws `DataCloneError` for functions, symbols and
67
- // class instances. Surface it as a domain invariant instead of leaking
68
- // a host-specific runtime error to callers building value objects.
82
+ // `structuredClone` throws `DataCloneError` for functions and symbols.
83
+ // (Class instances do NOT throw they are silently flattened to plain
84
+ // objects, prototype lost.) Surface the error as a domain invariant
85
+ // instead of leaking a host-specific runtime error to callers building
86
+ // value objects.
69
87
  throw new InvariantViolationException(
70
88
  `makeImmutable requires a structured-cloneable value: ${
71
89
  error instanceof Error ? error.message : String(error)