@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
package/dist/version.d.ts CHANGED
@@ -1,9 +1,36 @@
1
1
  //#region src/core/versioning/version.d.ts
2
+ /**
3
+ * A structural contract version in `MAJOR.MINOR` form (e.g. `"1.0"`, `"2.5"`).
4
+ *
5
+ * The template-literal type is intentionally loose — it also accepts strings
6
+ * the module rejects at runtime (e.g. `"1.0.0"` matches as `1` · `.` · `0.0`).
7
+ * The runtime regex in {@link version} is the real guard; the type is only a
8
+ * light hint.
9
+ */
2
10
  type ContractVersion = `${number}.${number}`;
11
+ /** Anything a class decorator can stamp a static property onto. */
3
12
  type VersionedTarget = {
4
13
  readonly prototype: object;
5
14
  };
6
15
  declare const CONTRACT_VERSION_PROPERTY: "CONTRACT_VERSION";
16
+ /**
17
+ * Class decorator that stamps a `MAJOR.MINOR` structural contract version onto
18
+ * the class as a static `CONTRACT_VERSION` property.
19
+ *
20
+ * The property is defined **non-writable, non-configurable and non-enumerable**
21
+ * on purpose: it must not be reassigned or redefined (tampering with a contract
22
+ * version silently corrupts migration decisions), and keeping it off enumeration
23
+ * excludes it from `JSON.stringify`/`for..in`. A consequence of
24
+ * `configurable: false` is that decorating the same class twice throws
25
+ * `TypeError: Cannot redefine property` — a deliberate fail-fast.
26
+ *
27
+ * Note on inheritance: a subclass that is not re-decorated inherits its base's
28
+ * `CONTRACT_VERSION` through the static prototype chain. Re-decorate any
29
+ * subclass whose persisted shape diverges from its base.
30
+ *
31
+ * @throws {InvariantViolationException} When `contractVersion` is not
32
+ * `MAJOR.MINOR` (validated at module-load time, when the decorator runs).
33
+ */
7
34
  declare function version<const TVersion extends ContractVersion>(contractVersion: TVersion): (target: VersionedTarget) => void;
8
35
  //#endregion
9
36
  export { version as i, ContractVersion as n, VersionedTarget as r, CONTRACT_VERSION_PROPERTY as t };
@@ -1,3 +1,3 @@
1
1
  import { i as version, n as ContractVersion, r as VersionedTarget, t as CONTRACT_VERSION_PROPERTY } from "../version.cjs";
2
- import { a as buildDomainEventContractVersions, i as DomainEventEnvelope, n as DomainEventContractSelection, o as createDomainEventEnvelope, r as DomainEventContractVersions, t as CreateDomainEventEnvelopeInput } from "../domain-event-contracts.cjs";
3
- export { CONTRACT_VERSION_PROPERTY, ContractVersion, CreateDomainEventEnvelopeInput, DomainEventContractSelection, DomainEventContractVersions, DomainEventEnvelope, VersionedTarget, buildDomainEventContractVersions, createDomainEventEnvelope, version };
2
+ import { a as DomainEventEnvelope, i as DomainEventContractVersions, n as CreateDomainEventEnvelopeInput, o as buildDomainEventContractVersions, r as DomainEventContractSelection, s as createDomainEventEnvelope, t as ContractVersioned } from "../domain-event-contracts.cjs";
3
+ export { CONTRACT_VERSION_PROPERTY, ContractVersion, ContractVersioned, CreateDomainEventEnvelopeInput, DomainEventContractSelection, DomainEventContractVersions, DomainEventEnvelope, VersionedTarget, buildDomainEventContractVersions, createDomainEventEnvelope, version };
@@ -1,3 +1,3 @@
1
1
  import { i as version, n as ContractVersion, r as VersionedTarget, t as CONTRACT_VERSION_PROPERTY } from "../version.js";
2
- import { a as buildDomainEventContractVersions, i as DomainEventEnvelope, n as DomainEventContractSelection, o as createDomainEventEnvelope, r as DomainEventContractVersions, t as CreateDomainEventEnvelopeInput } from "../domain-event-contracts.js";
3
- export { CONTRACT_VERSION_PROPERTY, ContractVersion, CreateDomainEventEnvelopeInput, DomainEventContractSelection, DomainEventContractVersions, DomainEventEnvelope, VersionedTarget, buildDomainEventContractVersions, createDomainEventEnvelope, version };
2
+ import { a as DomainEventEnvelope, i as DomainEventContractVersions, n as CreateDomainEventEnvelopeInput, o as buildDomainEventContractVersions, r as DomainEventContractSelection, s as createDomainEventEnvelope, t as ContractVersioned } from "../domain-event-contracts.js";
3
+ export { CONTRACT_VERSION_PROPERTY, ContractVersion, ContractVersioned, CreateDomainEventEnvelopeInput, DomainEventContractSelection, DomainEventContractVersions, DomainEventEnvelope, VersionedTarget, buildDomainEventContractVersions, createDomainEventEnvelope, version };
package/meta.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "2",
3
3
  "name": "erp-core",
4
- "version": "1.4.0",
4
+ "version": "2.0.0",
5
5
  "description": "Núcleo arquitetural para ERP em TypeScript — entidades, value objects, erros tipados e policies declaráveis sobre clean architecture.",
6
6
  "compatibility": {
7
7
  "engines": {
@@ -83,6 +83,7 @@
83
83
  "PermissionSet",
84
84
  "RbacAuthorizer",
85
85
  "AuthorizerPort",
86
+ "rbacContextFields",
86
87
  "AbacRule",
87
88
  "AbacPolicySet",
88
89
  "AbacAuthorizer",
@@ -105,7 +106,9 @@
105
106
  "1.1.0 - **New: `ResultRepository`, `UseCaseObservability`, and `./application` subpath**",
106
107
  "1.2.0 - Add zod-free subpaths for the stable core primitives and a value-object equality plugin system.",
107
108
  "1.3.0 - Add zod-free subpaths for the stable core primitives and a value-object equality plugin system.",
108
- "1.4.0 - Add a zod-free ABAC module and the `./abac` subpath."
109
+ "1.4.0 - Add a zod-free ABAC module and the `./abac` subpath.",
110
+ "1.5.0 - Add a zod-free ABAC module and the `./abac` subpath.",
111
+ "2.0.0 - Harden RBAC/ABAC, the policy condition engine, bitemporal primitives, the Result/Outcome module and the domain-exception hierarchy, plus fix several invariant bugs found while doing so."
109
112
  ],
110
113
  "deprecated": false
111
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cullet/erp-core",
3
- "version": "1.4.0",
3
+ "version": "2.0.0",
4
4
  "description": "Núcleo arquitetural para ERP em TypeScript — entidades, value objects, erros tipados e policies declaráveis sobre clean architecture.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -7,7 +7,7 @@ import type { AbacRequest } from "./abac-request.js";
7
7
  import { abacContext } from "./attributes.js";
8
8
  import { combine, type CombiningAlgorithm } from "./combining.js";
9
9
  import type { AbacPolicySet } from "./domain/policy-set.js";
10
- import type { AbacRule } from "./domain/rule.js";
10
+ import type { AbacRule, RuleEffect } from "./domain/rule.js";
11
11
 
12
12
  // ABAC reuses the gate engine's v1 condition matcher; the version stamps the
13
13
  // evaluation reports the shared reporter emits.
@@ -34,16 +34,30 @@ interface AbacDecision {
34
34
  * "errors as values" contract of `RbacAuthorizer`/`GateEngineV1`.
35
35
  *
36
36
  * A denial maps to {@link AuthorizationError.policyDenied}, attributed to the
37
- * deciding rule's `id`/`version`; a condition-evaluation failure (a missing
37
+ * deciding rule's `id`/`version`. A condition-evaluation failure (a missing
38
38
  * attribute, a wrong-typed operand) fails closed as
39
- * {@link AuthorizationError.forbidden}. Loading the dynamic attributes is the
40
- * consumer's job (behind an `AbacAuthorizerPort` adapter); this class only decides.
39
+ * {@link AuthorizationError.forbidden} also attributed to the culprit rule's
40
+ * `id`/`version` unless the set opts into `onEvaluationError: "skip-rule"`, in
41
+ * which case the unevaluable rule is skipped and the rest decide. Every resolved
42
+ * decision (PERMIT and DENY) is emitted best-effort through the configured
43
+ * reporter as an `abac-decision` event (silent by default). Timestamps come from
44
+ * an injectable `clock` (default `() => new Date()`), so the decisor is pure
45
+ * given one. Loading the dynamic attributes is the consumer's job (behind an
46
+ * `AbacAuthorizerPort` adapter); this class only decides.
41
47
  */
42
48
  class AbacAuthorizer {
43
49
  private readonly coreConfig: CoreConfig;
50
+ private readonly clock: () => Date;
44
51
 
45
- constructor(params: { readonly coreConfig?: CoreConfig } = {}) {
52
+ constructor(
53
+ params: {
54
+ readonly coreConfig?: CoreConfig;
55
+ /** Injectable clock for deterministic timestamps; defaults to `() => new Date()`. */
56
+ readonly clock?: () => Date;
57
+ } = {},
58
+ ) {
46
59
  this.coreConfig = params.coreConfig ?? coreConfig;
60
+ this.clock = params.clock ?? (() => new Date());
47
61
  }
48
62
 
49
63
  authorize(
@@ -60,12 +74,22 @@ class AbacAuthorizer {
60
74
  for (const rule of policies.rules) {
61
75
  const matched = evaluator.evaluate(rule.condition);
62
76
  if (matched.isErr()) {
63
- // Fail closed: a technical evaluation error is never a silent PERMIT.
77
+ if (policies.onEvaluationError === "skip-rule") {
78
+ // Escape valve for evolving rules: drop the unevaluable rule
79
+ // (the evaluator already reported the anomaly) and let the
80
+ // rest decide, instead of failing the whole set closed.
81
+ continue;
82
+ }
83
+ // Fail closed: a technical evaluation error is never a silent
84
+ // PERMIT. Attribute the deny to the culprit rule so the 403 is
85
+ // triageable without turning on the reporter.
64
86
  return Result.err(
65
87
  AuthorizationError.forbidden({
66
88
  action: request.action,
67
89
  resource: request.resource,
68
- actor: { userId: request.actor.raw },
90
+ actor: { actorId: request.actor.raw },
91
+ policyId: rule.id,
92
+ policyVersion: rule.version,
69
93
  details: matched.errorOrNull() ?? undefined,
70
94
  }),
71
95
  );
@@ -81,13 +105,15 @@ class AbacAuthorizer {
81
105
  policies.defaultEffect,
82
106
  );
83
107
 
108
+ this.reportDecision(request, policies, effect, decidingRule);
109
+
84
110
  if (effect === "PERMIT") {
85
111
  return Result.ok({
86
112
  action: request.action,
87
113
  effect: "PERMIT",
88
114
  matchedRule: decidingRule?.id ?? "<default>",
89
115
  algorithm: policies.algorithm,
90
- grantedAtIso: new Date().toISOString(),
116
+ grantedAtIso: this.clock().toISOString(),
91
117
  });
92
118
  }
93
119
 
@@ -95,14 +121,38 @@ class AbacAuthorizer {
95
121
  AuthorizationError.policyDenied({
96
122
  policyId: decidingRule?.id ?? "<default-deny>",
97
123
  policyVersion: decidingRule?.version ?? 0,
98
- evaluatedAtIso: new Date().toISOString(),
124
+ evaluatedAtIso: this.clock().toISOString(),
99
125
  action: request.action,
100
126
  resource: request.resource,
101
- actor: { userId: request.actor.raw },
127
+ actor: { actorId: request.actor.raw },
102
128
  reasonCode: decidingRule?.id,
103
129
  }),
104
130
  );
105
131
  }
132
+
133
+ // Best-effort audit trail: emits the resolved decision (PERMIT and DENY)
134
+ // through the configured reporter, silent by default.
135
+ private reportDecision(
136
+ request: AbacRequest,
137
+ policies: AbacPolicySet,
138
+ effect: RuleEffect,
139
+ decidingRule: AbacRule | undefined,
140
+ ): void {
141
+ this.coreConfig.reportSafely({
142
+ kind: "abac-decision",
143
+ level: "info",
144
+ action: request.action,
145
+ resource: request.resource,
146
+ actorId: request.actor.raw,
147
+ effect,
148
+ decidingRuleId:
149
+ decidingRule?.id ??
150
+ (effect === "PERMIT" ? "<default>" : "<default-deny>"),
151
+ decidingRuleVersion: decidingRule?.version,
152
+ algorithm: policies.algorithm,
153
+ occurredAt: this.clock(),
154
+ });
155
+ }
106
156
  }
107
157
 
108
158
  export { AbacAuthorizer, type AbacDecision };
@@ -1,41 +1,84 @@
1
+ import { ValidationCode } from "../../exceptions/validation-code.js";
2
+ import { InvalidValueException } from "../../exceptions/validation-exception.js";
3
+ import { ValidationField } from "../../exceptions/validation-field.js";
1
4
  import type { CombiningAlgorithm } from "../combining.js";
2
5
 
3
6
  import type { AbacRule, RuleEffect } from "./rule.js";
4
7
 
8
+ /**
9
+ * How the {@link AbacAuthorizer} reacts when a rule's condition cannot be
10
+ * evaluated (a referenced attribute is absent, an operand is wrong-typed).
11
+ *
12
+ * - `fail-closed` (default, secure): the whole decision fails closed as
13
+ * `forbidden`. Safe, but it means adding a rule that reads an attribute a call
14
+ * site does not populate makes that call site deny everything until it does.
15
+ * - `skip-rule`: the unevaluable rule is dropped from the applicable set and the
16
+ * remaining rules decide — the escape valve for evolving rules without
17
+ * trailing every call site. The evaluator still reports the anomaly.
18
+ */
19
+ export type OnEvaluationError = "fail-closed" | "skip-rule";
20
+
21
+ const POLICY_SET_FIELD = ValidationField.of("abacPolicySet");
22
+
5
23
  /**
6
24
  * An ordered collection of {@link AbacRule}s plus how to combine them
7
- * ({@link CombiningAlgorithm}) and what to decide when no rule applies
8
- * ({@link defaultEffect}). This is where ABAC goes beyond a single gate
25
+ * ({@link CombiningAlgorithm}), what to decide when no rule applies
26
+ * ({@link defaultEffect}), and how to react to an unevaluable rule
27
+ * ({@link onEvaluationError}). This is where ABAC goes beyond a single gate
9
28
  * condition: several PERMIT/DENY rules resolved by an explicit algorithm.
10
29
  *
11
- * Closed by default — the combining algorithm defaults to `"deny-overrides"` and
12
- * the default effect to `"DENY"`, so a request matching nothing is denied. A
13
- * plain holder (not a value object); build through {@link of}.
30
+ * Closed by default — the combining algorithm defaults to `"deny-overrides"`,
31
+ * the default effect to `"DENY"`, and evaluation errors to `"fail-closed"`, so a
32
+ * request matching nothing (or hitting a technical error) is denied. A plain
33
+ * holder (not a value object); build through {@link of}.
14
34
  */
15
35
  class AbacPolicySet {
16
36
  private constructor(
17
37
  private readonly _rules: readonly AbacRule[],
18
38
  private readonly _algorithm: CombiningAlgorithm,
19
39
  private readonly _defaultEffect: RuleEffect,
40
+ private readonly _onEvaluationError: OnEvaluationError,
20
41
  ) {
21
42
  Object.freeze(this._rules);
22
43
  Object.freeze(this);
23
44
  }
24
45
 
46
+ /**
47
+ * Builds a policy set. Rejects duplicate rule ids with
48
+ * {@link InvalidValueException} so a denial's `policyId` attribution is
49
+ * unambiguous.
50
+ */
25
51
  static of(
26
52
  rules: readonly AbacRule[],
27
53
  options: {
28
54
  readonly algorithm?: CombiningAlgorithm;
29
55
  readonly defaultEffect?: RuleEffect;
56
+ readonly onEvaluationError?: OnEvaluationError;
30
57
  } = {},
31
58
  ): AbacPolicySet {
59
+ AbacPolicySet.assertUniqueIds(rules);
32
60
  return new AbacPolicySet(
33
61
  [...rules],
34
62
  options.algorithm ?? "deny-overrides",
35
63
  options.defaultEffect ?? "DENY",
64
+ options.onEvaluationError ?? "fail-closed",
36
65
  );
37
66
  }
38
67
 
68
+ private static assertUniqueIds(rules: readonly AbacRule[]): void {
69
+ const seen = new Set<string>();
70
+ for (const rule of rules) {
71
+ if (seen.has(rule.id)) {
72
+ throw new InvalidValueException(
73
+ POLICY_SET_FIELD.nested("rules"),
74
+ ValidationCode.ALREADY_EXISTS,
75
+ `abac policy set has duplicate rule id "${rule.id}"; ids must be unique for unambiguous audit attribution`,
76
+ );
77
+ }
78
+ seen.add(rule.id);
79
+ }
80
+ }
81
+
39
82
  get rules(): readonly AbacRule[] {
40
83
  return this._rules;
41
84
  }
@@ -47,6 +90,10 @@ class AbacPolicySet {
47
90
  get defaultEffect(): RuleEffect {
48
91
  return this._defaultEffect;
49
92
  }
93
+
94
+ get onEvaluationError(): OnEvaluationError {
95
+ return this._onEvaluationError;
96
+ }
50
97
  }
51
98
 
52
99
  export { AbacPolicySet };
@@ -2,9 +2,10 @@ import { ValidationCode } from "../../exceptions/validation-code.js";
2
2
  import { InvalidValueException } from "../../exceptions/validation-exception.js";
3
3
  import { ValidationField } from "../../exceptions/validation-field.js";
4
4
  import { ValueObject } from "../../domain/value-object.js";
5
- import type {
6
- ConditionNode,
7
- ConditionOp,
5
+ import {
6
+ CONDITION_OPS,
7
+ type ConditionNode,
8
+ type ConditionOp,
8
9
  } from "../../policies/engines/v1/condition-types.js";
9
10
 
10
11
  /** Whether a matching {@link AbacRule} permits or denies the action. */
@@ -26,18 +27,9 @@ type AbacRuleProps = {
26
27
 
27
28
  const RULE_FIELD = ValidationField.of("abacRule");
28
29
 
29
- const CONDITION_OPS = new Set<ConditionOp>([
30
- "eq",
31
- "neq",
32
- "gt",
33
- "gte",
34
- "lt",
35
- "lte",
36
- "in",
37
- "notIn",
38
- "isNull",
39
- "isNotNull",
40
- ]);
30
+ // Derived from the evaluator's own operator list (single source of truth) so a
31
+ // new operator becomes usable in ABAC rules without a parallel edit here.
32
+ const SUPPORTED_OPS = new Set<ConditionOp>(CONDITION_OPS);
41
33
 
42
34
  const RULE_EFFECTS = new Set<RuleEffect>(["PERMIT", "DENY"]);
43
35
 
@@ -57,6 +49,26 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
57
49
  * authored in TypeScript — trusted data, not untrusted JSON — the structural
58
50
  * check is a hand-rolled walk of the condition tree rather than a schema parse.
59
51
  * Build one through {@link of}; the constructor is private.
52
+ *
53
+ * **Attribute semantics a rule author must know (the condition is evaluated by
54
+ * the shared gate/compute engine, whose runtime rules apply here too):**
55
+ * - **A referenced attribute that is *absent* from the request is a technical
56
+ * evaluation error, not a non-match.** By default (`onEvaluationError:
57
+ * "fail-closed"` on the {@link AbacPolicySet}) that error fails the *entire*
58
+ * decision closed — so adding a rule that reads an attribute a given call site
59
+ * does not yet populate makes that call site start returning `forbidden`,
60
+ * *even for requests the older rules used to permit*. Choose
61
+ * `onEvaluationError: "skip-rule"` on the set to skip an unevaluable rule
62
+ * instead of failing the whole set.
63
+ * - **`isNull` / `isNotNull` test for an explicit `null`/`undefined` *value*, not
64
+ * for a missing key.** A key that is absent from the bag never reaches the
65
+ * operator — it short-circuits to the missing-attribute error above. To match
66
+ * "no deletedAt", the consumer must put `deletedAt: null` in the bag, not omit
67
+ * it. Pass `allowNull: true` on a relational leaf to treat a present `null` as
68
+ * a non-match instead of an error.
69
+ * - **Date comparison operands must be strict ISO-8601 UTC**
70
+ * (`YYYY-MM-DDTHH:mm:ss.sssZ`); a bare `"2026-07-09"` or an offset like
71
+ * `+00:00` is rejected as an invalid operand and fails closed.
60
72
  */
61
73
  class AbacRule extends ValueObject<AbacRuleProps, AbacRuleProps> {
62
74
  private constructor(props: AbacRuleProps) {
@@ -138,7 +150,7 @@ class AbacRule extends ValueObject<AbacRuleProps, AbacRuleProps> {
138
150
  }
139
151
  if (
140
152
  typeof node.op !== "string" ||
141
- !CONDITION_OPS.has(node.op as ConditionOp)
153
+ !SUPPORTED_OPS.has(node.op as ConditionOp)
142
154
  ) {
143
155
  AbacRule.rejectCondition(
144
156
  `abac rule condition leaf "op" is not a supported operator, got "${String(node.op)}"`,
@@ -2,6 +2,12 @@
2
2
  // combining algorithms, the attribute→context flattening, the pure decisor, the
3
3
  // optional RBAC+ABAC composite, and the AbacAuthorizerPort seam (which physically
4
4
  // lives under application/ports/ but is surfaced here as the ABAC public home).
5
+ //
6
+ // Rule-author footguns (documented in full on AbacRule): a referenced attribute
7
+ // absent from the request is a technical error that, by default, fails the whole
8
+ // decision closed — set `onEvaluationError: "skip-rule"` on the AbacPolicySet to
9
+ // skip the unevaluable rule instead. `isNull`/`isNotNull` match an explicit
10
+ // `null` value, never a missing key.
5
11
 
6
12
  export type { AbacAuthorizerPort } from "../application/ports/abac-authorizer.port.js";
7
13
 
@@ -13,7 +19,7 @@ export {
13
19
  type CompositeAccessRequest,
14
20
  CompositeAuthorizer,
15
21
  } from "./composite-authorizer.js";
16
- export { AbacPolicySet } from "./domain/policy-set.js";
22
+ export { AbacPolicySet, type OnEvaluationError } from "./domain/policy-set.js";
17
23
  export {
18
24
  AbacRule,
19
25
  type AbacRuleProps,
@@ -1,5 +1,6 @@
1
1
  import { UseCase } from "../use-case.js";
2
2
  import { version } from "../../versioning/version.js";
3
+ import type { TraceAttributeValue } from "../ports/tracer.port.js";
3
4
  import type { Result } from "../../result/result.js";
4
5
 
5
6
  import { RequestedBy } from "./requested-by.js";
@@ -22,7 +23,19 @@ interface CommandInput {
22
23
  abstract class Command<
23
24
  Input extends CommandInput,
24
25
  Output extends Result<unknown, unknown> = Result<void, never>,
25
- > extends UseCase<Input, Output> {}
26
+ > extends UseCase<Input, Output> {
27
+ /**
28
+ * Surfaces the actor's *kind* (`"user"` / `"system"`) on the span and
29
+ * metrics so a write can be sliced by origin. Only the low-cardinality
30
+ * `kind` is emitted — never the raw id, which would blow up label
31
+ * cardinality and could leak a user identifier into metrics.
32
+ */
33
+ protected override spanAttributes(
34
+ input: Input,
35
+ ): Readonly<Record<string, TraceAttributeValue>> {
36
+ return { "requested_by.kind": input.requestedBy.kind };
37
+ }
38
+ }
26
39
 
27
40
  export type { CommandInput };
28
41
  export { Command };
@@ -1,10 +1,15 @@
1
1
  import { ValidationCode } from "../../exceptions/validation-code.js";
2
2
  import { InvalidValueException } from "../../exceptions/validation-exception.js";
3
3
  import { ValidationField } from "../../exceptions/validation-field.js";
4
+ import { UUID_PATTERN } from "../../shared/uuid.js";
4
5
 
5
6
  // Identifies who triggered a Command.
6
7
  // Two valid formats:
7
- // - Human user: UUID v4 (e.g.: "550e8400-e29b-41d4-a716-446655440000")
8
+ // - Human user: canonical RFC-4122 UUID, versions 1–8 (including the
9
+ // time-ordered v7) — the same pattern `UuidIdentifier` enforces
10
+ // (e.g.: "550e8400-e29b-41d4-a716-446655440000"). The constraint is
11
+ // single-sourced in `shared/uuid.ts`, so `UuidIdentifier` and this value
12
+ // always accept the same set of versions.
8
13
  // - System identity: "system:<job>" where <job> is [a-z][a-z0-9]*(-[a-z0-9]+)*
9
14
  // (e.g.: "system:late-fee-job", "system:email-sender")
10
15
  //
@@ -15,9 +20,6 @@ type RequestedByKind = "user" | "system";
15
20
 
16
21
  const REQUESTED_BY_FIELD = ValidationField.of("requestedBy");
17
22
 
18
- const UUID_PATTERN =
19
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20
-
21
23
  // system:<job> where <job> starts with a lowercase letter and may have hyphens between segments
22
24
  const SYSTEM_IDENTITY_PATTERN = /^system:[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
23
25
 
@@ -27,7 +29,13 @@ class RequestedBy {
27
29
 
28
30
  private constructor(kind: RequestedByKind, raw: string) {
29
31
  this.kind = kind;
30
- this.raw = raw;
32
+ // UUIDs are matched case-insensitively (see `shared/uuid.ts`), so the
33
+ // same user id can arrive lowercased from Postgres and uppercased from a
34
+ // JWT. Canonicalize user ids to lowercase here — the single choke point
35
+ // every factory passes through — so exact-string comparators downstream
36
+ // (`Grant.appliesTo`, `rbacContextFields`) never miss the right identity
37
+ // on case alone. System identities are already lowercase by pattern.
38
+ this.raw = kind === "user" ? raw.toLowerCase() : raw;
31
39
  Object.freeze(this);
32
40
  }
33
41
 
@@ -15,6 +15,7 @@ export type {
15
15
  PolicyPortError,
16
16
  Repository,
17
17
  ResultRepository,
18
+ ResultTemporalRepository,
18
19
  TraceAttributeValue,
19
20
  TraceSpan,
20
21
  TracerPort,
@@ -22,7 +23,7 @@ export type {
22
23
  TemporalRepository,
23
24
  } from "./ports/index.js";
24
25
  export { mapPolicyEvaluationError } from "./policy-error-mapper.js";
25
- export type { CacheStrategy, Page } from "./queries/index.js";
26
+ export type { CacheStrategy, Page, QueryOutput } from "./queries/index.js";
26
27
  export { Query } from "./queries/index.js";
27
28
  export {
28
29
  assertTemporalContext,
@@ -49,7 +49,13 @@ export function mapPolicyEvaluationError(err: PolicyEvaluationError): AppError {
49
49
  { cause: err.cause },
50
50
  );
51
51
  case "ENGINE_FAILURE":
52
+ // `engine`/`engineVersion` are internal detail kept in `metadata`
53
+ // for logs. `publicMessage` gives the HTTP boundary a safe string
54
+ // to expose so it never has to serialize this metadata to a client
55
+ // on a 500. (Whether the boundary leaks `metadata` is still its
56
+ // call — this only hands it the non-leaking alternative.)
52
57
  return new UnexpectedError(err.message, err.cause, {
58
+ publicMessage: "Policy evaluation failed unexpectedly.",
53
59
  metadata: {
54
60
  policyKey: err.policyKey,
55
61
  engine: err.engine,
@@ -8,6 +8,7 @@ export type {
8
8
  } from "./policy-port.js";
9
9
  export type { Repository, ResultRepository } from "./repository.port.js";
10
10
  export type {
11
+ ResultTemporalRepository,
11
12
  TemporalHistory,
12
13
  TemporalRepository,
13
14
  } from "./temporal-repository.port.js";
@@ -16,3 +17,9 @@ export type {
16
17
  TraceSpan,
17
18
  TracerPort,
18
19
  } from "./tracer.port.js";
20
+
21
+ // Note: `AuthorizerPort` (RBAC) and `AbacAuthorizerPort` (ABAC) also live in
22
+ // this folder but are intentionally NOT re-exported here — they belong to the
23
+ // `rbac` / `abac` surfaces and are exported from those barrels instead (each
24
+ // port file documents why). Import them from `cullet/erp-core` rbac/abac
25
+ // entry points, not from this ports barrel.
@@ -1,9 +1,21 @@
1
1
  import type { TemporalSnapshot } from "../../domain/temporal/index.js";
2
+ import type { AppError } from "../../errors/index.js";
3
+ import type { Result } from "../../result/result.js";
2
4
 
3
- import type { Repository } from "./repository.port.js";
5
+ import type { Repository, ResultRepository } from "./repository.port.js";
4
6
 
5
7
  type TemporalHistory<TEntity> = readonly TemporalSnapshot<TEntity>[];
6
8
 
9
+ /**
10
+ * Bitemporal persistence port in the imperative (exception-raising) style.
11
+ *
12
+ * `delete(id)` is inherited from {@link Repository} and its bitemporal meaning
13
+ * is intentionally left to the implementation: it may close the current
14
+ * valid-time interval (a logical end-of-validity that preserves history) or
15
+ * physically purge the row's whole history. Pick one per adapter and document
16
+ * it — callers cannot tell which from this type. Prefer closing validity so
17
+ * `findAsOf`/`findHistory` stay meaningful for past instants.
18
+ */
7
19
  interface TemporalRepository<TEntity, TId> extends Repository<TEntity, TId> {
8
20
  findAsOf(id: TId, asOf: Date): Promise<TEntity | null>;
9
21
  findAtTransaction(id: TId, txTime: Date): Promise<TEntity | null>;
@@ -11,4 +23,25 @@ interface TemporalRepository<TEntity, TId> extends Repository<TEntity, TId> {
11
23
  save(entity: TEntity, validFrom?: Date): Promise<void>;
12
24
  }
13
25
 
14
- export type { TemporalHistory, TemporalRepository };
26
+ /**
27
+ * `Result`-returning counterpart of {@link TemporalRepository}, aligned with the
28
+ * "errors as values" philosophy (mirrors the {@link Repository} /
29
+ * {@link ResultRepository} pair). The `delete` semantics note above applies
30
+ * unchanged.
31
+ */
32
+ interface ResultTemporalRepository<TEntity, TId, TError = AppError>
33
+ extends ResultRepository<TEntity, TId, TError> {
34
+ findAsOf(id: TId, asOf: Date): Promise<Result<TEntity | null, TError>>;
35
+ findAtTransaction(
36
+ id: TId,
37
+ txTime: Date,
38
+ ): Promise<Result<TEntity | null, TError>>;
39
+ findHistory(id: TId): Promise<Result<TemporalHistory<TEntity>, TError>>;
40
+ save(entity: TEntity, validFrom?: Date): Promise<Result<void, TError>>;
41
+ }
42
+
43
+ export type {
44
+ ResultTemporalRepository,
45
+ TemporalHistory,
46
+ TemporalRepository,
47
+ };
@@ -1,2 +1,2 @@
1
- export type { CacheStrategy, Page } from "./query.js";
1
+ export type { CacheStrategy, Page, QueryOutput } from "./query.js";
2
2
  export { Query } from "./query.js";
@@ -4,12 +4,15 @@ import type { AppError } from "../../errors/index.js";
4
4
  import type { Result } from "../../result/result.js";
5
5
 
6
6
  /**
7
- * Paginated result of a list query.
7
+ * Paginated result of a list query (offset-based).
8
8
  * Use as `Output` when the query returns a collection with pagination metadata.
9
+ * For cursor/keyset pagination, model your own `Output` type instead.
9
10
  */
10
11
  interface Page<T> {
11
12
  readonly items: readonly T[];
13
+ /** Total rows across all pages, ignoring `page`/`pageSize`. */
12
14
  readonly total: number;
15
+ /** 1-based page number (the first page is `1`, not `0`). */
13
16
  readonly page: number;
14
17
  readonly pageSize: number;
15
18
  }
@@ -17,11 +20,25 @@ interface Page<T> {
17
20
  /**
18
21
  * Cache strategy declared by the query type.
19
22
  * Infrastructure reads this value to decide whether and how to cache the result.
23
+ *
24
+ * These are pure descriptors: the type cannot enforce that a duration is finite
25
+ * and positive, so the caching adapter must validate `ttlMs` /
26
+ * `staleWhileRevalidateMs` (`> 0`, finite) before honoring the strategy.
20
27
  */
21
28
  type CacheStrategy =
22
29
  | { readonly kind: "NO_CACHE" }
23
30
  | { readonly kind: "TIME_TO_LIVE"; readonly ttlMs: number }
24
- | { readonly kind: "STALE_WHILE_REVALIDATE"; readonly ttlMs: number };
31
+ | {
32
+ readonly kind: "STALE_WHILE_REVALIDATE";
33
+ /** How long the entry is served fresh. */
34
+ readonly ttlMs: number;
35
+ /**
36
+ * How long *after* `ttlMs` a stale entry may still be served while a
37
+ * background refresh runs. SWR needs both windows; a single TTL is
38
+ * ambiguous and each adapter would guess the second one.
39
+ */
40
+ readonly staleWhileRevalidateMs: number;
41
+ };
25
42
 
26
43
  type QueryOutput = object | string | number | boolean | bigint | symbol | null;
27
44
 
@@ -44,7 +61,12 @@ abstract class Query<
44
61
  Data extends QueryOutput = never,
45
62
  Failure = AppError,
46
63
  > extends UseCase<Input, Result<Data, Failure>> {
47
- protected cacheStrategy(): CacheStrategy {
64
+ /**
65
+ * Declares how infrastructure should cache this query's result. Public so
66
+ * a caching adapter can actually read it off the instance; overrides must
67
+ * stay public.
68
+ */
69
+ public cacheStrategy(): CacheStrategy {
48
70
  return { kind: "NO_CACHE" };
49
71
  }
50
72
  }