@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.
- package/KIT_CONTEXT.md +7 -1
- package/dist/abac/index.d.cts +2 -2
- package/dist/abac/index.d.ts +2 -2
- package/dist/aggregate-version.cjs +14 -0
- package/dist/aggregate-version.cjs.map +1 -0
- package/dist/aggregate-version.js +9 -0
- package/dist/aggregate-version.js.map +1 -0
- package/dist/app-error.cjs +21 -6
- package/dist/app-error.cjs.map +1 -1
- package/dist/app-error.js +21 -6
- package/dist/app-error.js.map +1 -1
- package/dist/application/index.cjs +8 -4
- package/dist/application/index.d.cts +2 -2
- package/dist/application/index.d.ts +2 -2
- package/dist/application/index.js +1 -2
- package/dist/authorization-error.cjs +67 -17
- package/dist/authorization-error.cjs.map +1 -1
- package/dist/authorization-error.d.cts +6 -2
- package/dist/authorization-error.d.ts +6 -2
- package/dist/authorization-error.js +50 -18
- package/dist/authorization-error.js.map +1 -1
- package/dist/authorizer.port.d.cts +18 -3
- package/dist/authorizer.port.d.ts +18 -3
- package/dist/composite-authorizer.d.cts +63 -10
- package/dist/composite-authorizer.d.ts +63 -10
- package/dist/condition-evaluator.cjs +117 -172
- package/dist/condition-evaluator.cjs.map +1 -1
- package/dist/condition-evaluator.js +118 -167
- package/dist/condition-evaluator.js.map +1 -1
- package/dist/core-config.d.cts +53 -6
- package/dist/core-config.d.ts +53 -6
- package/dist/decorate.cjs +14 -0
- package/dist/decorate.js +9 -0
- package/dist/domain/index.cjs +107 -2
- package/dist/domain/index.cjs.map +1 -0
- package/dist/domain/index.d.cts +25 -1
- package/dist/domain/index.d.ts +25 -1
- package/dist/domain/index.js +99 -3
- package/dist/domain/index.js.map +1 -0
- package/dist/domain-event-contracts.cjs +12 -8
- package/dist/domain-event-contracts.cjs.map +1 -1
- package/dist/domain-event-contracts.d.cts +29 -4
- package/dist/domain-event-contracts.d.ts +29 -4
- package/dist/domain-event-contracts.js +11 -7
- package/dist/domain-event-contracts.js.map +1 -1
- package/dist/domain-exception.cjs +2 -1
- package/dist/domain-exception.cjs.map +1 -1
- package/dist/domain-exception.js +2 -1
- package/dist/domain-exception.js.map +1 -1
- package/dist/errors/index.cjs +3 -2
- package/dist/errors/index.d.cts +1 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +3 -2
- package/dist/exceptions/index.cjs +2 -2
- package/dist/exceptions/index.d.cts +1 -2
- package/dist/exceptions/index.d.ts +1 -2
- package/dist/exceptions/index.js +2 -2
- package/dist/gate-engine-registry.cjs.map +1 -1
- package/dist/gate-engine-registry.d.cts +1 -1
- package/dist/gate-engine-registry.d.ts +1 -1
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.cjs +11 -6
- package/dist/gate-v1-payload.schema.cjs.map +1 -1
- package/dist/gate-v1-payload.schema.js +11 -6
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/hashing.cjs +2 -44
- package/dist/hashing.cjs.map +1 -1
- package/dist/hashing.js +3 -39
- package/dist/hashing.js.map +1 -1
- package/dist/immutable.cjs +25 -12
- package/dist/immutable.cjs.map +1 -1
- package/dist/immutable.js +24 -11
- package/dist/immutable.js.map +1 -1
- package/dist/index.cjs +15 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -10
- package/dist/index.d.ts +9 -10
- package/dist/index.js +8 -9
- package/dist/index.js.map +1 -1
- package/dist/invalid-state-transition-exception.cjs +6 -6
- package/dist/invalid-state-transition-exception.cjs.map +1 -1
- package/dist/invalid-state-transition-exception.js +6 -6
- package/dist/invalid-state-transition-exception.js.map +1 -1
- package/dist/invariant-violation-exception.cjs +2 -2
- package/dist/invariant-violation-exception.cjs.map +1 -1
- package/dist/invariant-violation-exception.js +2 -2
- package/dist/invariant-violation-exception.js.map +1 -1
- package/dist/not-found-error.cjs +6 -4
- package/dist/not-found-error.cjs.map +1 -1
- package/dist/not-found-error.js +6 -4
- package/dist/not-found-error.js.map +1 -1
- package/dist/outcome.cjs +5 -5
- package/dist/outcome.cjs.map +1 -1
- package/dist/outcome.js +5 -5
- package/dist/outcome.js.map +1 -1
- package/dist/parse-gate-payload.d.cts +1 -1
- package/dist/parse-gate-payload.d.ts +1 -1
- package/dist/path.d.cts +2 -2
- package/dist/path.d.ts +2 -2
- package/dist/plugin.cjs +23 -13
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +22 -8
- package/dist/plugin.d.ts +22 -8
- package/dist/plugin.js +23 -13
- package/dist/plugin.js.map +1 -1
- package/dist/policies/engines/index.d.cts +1 -1
- package/dist/policies/engines/index.d.ts +1 -1
- package/dist/policies/engines/v1/gate/index.d.cts +3 -13
- package/dist/policies/engines/v1/gate/index.d.ts +3 -13
- package/dist/policies/index.d.cts +1 -1
- package/dist/policies/index.d.ts +1 -1
- package/dist/policy-bridge.cjs +30 -2
- package/dist/policy-bridge.cjs.map +1 -1
- package/dist/policy-bridge.d.cts +18 -0
- package/dist/policy-bridge.d.ts +18 -0
- package/dist/policy-bridge.js +30 -2
- package/dist/policy-bridge.js.map +1 -1
- package/dist/policy-service.cjs +41 -46
- package/dist/policy-service.cjs.map +1 -1
- package/dist/policy-service.d.cts +72 -9
- package/dist/policy-service.d.ts +72 -9
- package/dist/policy-service.js +43 -48
- package/dist/policy-service.js.map +1 -1
- package/dist/requested-by.cjs +4 -4
- package/dist/requested-by.cjs.map +1 -1
- package/dist/requested-by.js +2 -2
- package/dist/requested-by.js.map +1 -1
- package/dist/result.cjs +29 -1
- package/dist/result.cjs.map +1 -1
- package/dist/result.d.cts +12 -0
- package/dist/result.d.ts +12 -0
- package/dist/result.js +29 -1
- package/dist/result.js.map +1 -1
- package/dist/rule.cjs +94 -24
- package/dist/rule.cjs.map +1 -1
- package/dist/rule.js +94 -24
- package/dist/rule.js.map +1 -1
- package/dist/ruleset-registry.cjs +19 -0
- package/dist/ruleset-registry.cjs.map +1 -1
- package/dist/ruleset-registry.js +19 -0
- package/dist/ruleset-registry.js.map +1 -1
- package/dist/stable-stringify.cjs +87 -0
- package/dist/stable-stringify.cjs.map +1 -0
- package/dist/stable-stringify.js +76 -0
- package/dist/stable-stringify.js.map +1 -0
- package/dist/temporal-snapshot.d.cts +87 -0
- package/dist/temporal-snapshot.d.ts +87 -0
- package/dist/temporal-use-case.cjs +174 -16
- package/dist/temporal-use-case.cjs.map +1 -1
- package/dist/temporal-use-case.d.cts +82 -44
- package/dist/temporal-use-case.d.ts +82 -44
- package/dist/temporal-use-case.js +167 -15
- package/dist/temporal-use-case.js.map +1 -1
- package/dist/unexpected-error.cjs +4 -2
- package/dist/unexpected-error.cjs.map +1 -1
- package/dist/unexpected-error.js +4 -2
- package/dist/unexpected-error.js.map +1 -1
- package/dist/uuid-identifier.cjs +141 -8
- package/dist/uuid-identifier.cjs.map +1 -1
- package/dist/uuid-identifier.d.cts +26 -7
- package/dist/uuid-identifier.d.ts +26 -7
- package/dist/uuid-identifier.js +135 -8
- package/dist/uuid-identifier.js.map +1 -1
- package/dist/uuid.cjs +23 -0
- package/dist/uuid.cjs.map +1 -0
- package/dist/uuid.js +18 -0
- package/dist/uuid.js.map +1 -0
- package/dist/validation-code.cjs +9 -0
- package/dist/validation-code.cjs.map +1 -1
- package/dist/validation-code.d.cts +3 -0
- package/dist/validation-code.d.ts +3 -0
- package/dist/validation-code.js +9 -0
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.cjs +42 -67
- package/dist/validation-error.cjs.map +1 -1
- package/dist/validation-error.d.cts +29 -8
- package/dist/validation-error.d.ts +29 -8
- package/dist/validation-error.js +41 -66
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-exception.cjs +17 -6
- package/dist/validation-exception.cjs.map +1 -1
- package/dist/validation-exception.d.cts +33 -9
- package/dist/validation-exception.d.ts +33 -9
- package/dist/validation-exception.js +17 -6
- package/dist/validation-exception.js.map +1 -1
- package/dist/validation-field.cjs +3 -0
- package/dist/validation-field.cjs.map +1 -1
- package/dist/validation-field.d.cts +1 -0
- package/dist/validation-field.d.ts +1 -0
- package/dist/validation-field.js +3 -0
- package/dist/validation-field.js.map +1 -1
- package/dist/value-object-ruleset.contracts.d.cts +10 -0
- package/dist/value-object-ruleset.contracts.d.ts +10 -0
- package/dist/value-object.cjs +23 -4
- package/dist/value-object.cjs.map +1 -1
- package/dist/value-object.d.cts +25 -1
- package/dist/value-object.d.ts +25 -1
- package/dist/value-object.js +23 -4
- package/dist/value-object.js.map +1 -1
- package/dist/version.d.cts +27 -0
- package/dist/version.d.ts +27 -0
- package/dist/versioning/index.d.cts +2 -2
- package/dist/versioning/index.d.ts +2 -2
- package/meta.json +5 -2
- package/package.json +1 -1
- package/src/core/abac/authorizer.ts +60 -10
- package/src/core/abac/domain/policy-set.ts +52 -5
- package/src/core/abac/domain/rule.ts +28 -16
- package/src/core/abac/index.ts +7 -1
- package/src/core/application/commands/command.ts +14 -1
- package/src/core/application/commands/requested-by.ts +13 -5
- package/src/core/application/index.ts +2 -1
- package/src/core/application/policy-error-mapper.ts +6 -0
- package/src/core/application/ports/index.ts +7 -0
- package/src/core/application/ports/temporal-repository.port.ts +35 -2
- package/src/core/application/queries/index.ts +1 -1
- package/src/core/application/queries/query.ts +25 -3
- package/src/core/application/temporal/temporal-use-case.ts +31 -4
- package/src/core/application/use-case.ts +45 -8
- package/src/core/config/core-config.ts +46 -25
- package/src/core/config/index.ts +1 -0
- package/src/core/config/policy-reporter.ts +32 -1
- package/src/core/domain/entity.ts +51 -8
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +0 -2
- package/src/core/domain/rulesets/ruleset-registry.ts +24 -0
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +1 -7
- package/src/core/domain/temporal/half-open-interval.ts +34 -0
- package/src/core/domain/temporal/temporal-snapshot.ts +13 -2
- package/src/core/domain/temporal/transaction-time.ts +19 -15
- package/src/core/domain/temporal/valid-time.ts +20 -15
- package/src/core/domain/uuid-identifier.ts +6 -11
- package/src/core/domain/value-object.ts +29 -3
- package/src/core/errors/authentication-error.ts +5 -31
- package/src/core/errors/authorization-error.ts +12 -20
- package/src/core/errors/business-rule-violation-error.ts +4 -1
- package/src/core/errors/idempotency-error.ts +5 -2
- package/src/core/errors/index.ts +4 -0
- package/src/core/errors/integration-error.ts +4 -24
- package/src/core/errors/legacy-incompatible-error.ts +1 -0
- package/src/core/errors/not-found-error.ts +4 -1
- package/src/core/errors/temporal-error.ts +22 -20
- package/src/core/errors/unexpected-error.ts +5 -1
- package/src/core/errors/utils/factory-helpers.ts +70 -0
- package/src/core/errors/utils/index.ts +5 -0
- package/src/core/errors/utils/json-safe.ts +35 -12
- package/src/core/errors/validation-error.ts +4 -1
- package/src/core/exceptions/business-rule-violation-exception.ts +2 -1
- package/src/core/exceptions/domain-exception.ts +9 -1
- package/src/core/exceptions/entity-not-found-exception.ts +5 -1
- package/src/core/exceptions/invalid-state-transition-exception.ts +5 -2
- package/src/core/exceptions/invariant-violation-exception.ts +2 -2
- package/src/core/exceptions/validation-code.ts +14 -0
- package/src/core/exceptions/validation-exception.ts +44 -5
- package/src/core/exceptions/validation-field.ts +11 -1
- package/src/core/plugins/plugin.ts +25 -15
- package/src/core/plugins/types.ts +4 -2
- package/src/core/policies/asof/asof.ts +12 -0
- package/src/core/policies/catalog/policy-catalog-entry.ts +3 -1
- package/src/core/policies/catalog/policy-catalog.ts +5 -1
- package/src/core/policies/context/context-builder.ts +20 -0
- package/src/core/policies/context/context-resolver.ts +5 -0
- package/src/core/policies/context/context-seed.ts +11 -0
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +0 -2
- package/src/core/policies/engines/parse-gate-payload.ts +9 -0
- package/src/core/policies/engines/v1/condition-date-operands.ts +112 -0
- package/src/core/policies/engines/v1/condition-evaluator.ts +120 -291
- package/src/core/policies/engines/v1/condition-schema.ts +23 -19
- package/src/core/policies/engines/v1/condition-types.ts +18 -11
- package/src/core/policies/index.ts +4 -6
- package/src/core/policies/service/policy-service.ts +46 -35
- package/src/core/policies/utils/hash.ts +5 -69
- package/src/core/policies/utils/result.ts +1 -1
- package/src/core/rbac/access-request.ts +12 -2
- package/src/core/rbac/authorizer.ts +8 -1
- package/src/core/rbac/domain/role.ts +20 -0
- package/src/core/rbac/domain/scope.ts +6 -1
- package/src/core/result/index.ts +5 -0
- package/src/core/result/outcome.ts +5 -7
- package/src/core/result/result.ts +34 -1
- package/src/core/shared/hashing.ts +6 -57
- package/src/core/shared/immutable.ts +22 -4
- package/src/core/shared/stable-stringify.ts +144 -0
- package/src/core/shared/uuid.ts +16 -0
- package/src/core/versioning/domain-event-contracts.ts +43 -15
- package/src/core/versioning/index.ts +1 -0
- package/src/core/versioning/version.ts +30 -1
- package/src/domain/index.ts +5 -0
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +1 -1
- package/src/examples/rulesets/entity/order-invariants-v1.ts +2 -4
- package/src/examples/rulesets/entity/order-invariants-v2.ts +2 -4
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +2 -4
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +2 -4
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +2 -4
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +2 -4
- package/src/result/index.ts +7 -2
- package/src/version.ts +1 -1
- package/dist/domain-exception.d.cts +0 -7
- package/dist/domain-exception.d.ts +0 -7
- package/dist/entity.cjs +0 -126
- package/dist/entity.cjs.map +0 -1
- package/dist/entity.js +0 -115
- package/dist/entity.js.map +0 -1
- package/dist/use-case.cjs +0 -96
- package/dist/use-case.cjs.map +0 -1
- package/dist/use-case.js +0 -91
- 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
|
|
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
|
|
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": "
|
|
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
|
@@ -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
|
|
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}
|
|
40
|
-
*
|
|
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(
|
|
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
|
-
|
|
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: {
|
|
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:
|
|
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:
|
|
124
|
+
evaluatedAtIso: this.clock().toISOString(),
|
|
99
125
|
action: request.action,
|
|
100
126
|
resource: request.resource,
|
|
101
|
-
actor: {
|
|
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})
|
|
8
|
-
* ({@link defaultEffect})
|
|
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"
|
|
12
|
-
* the default effect to `"DENY"`,
|
|
13
|
-
*
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
!
|
|
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)}"`,
|
package/src/core/abac/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| {
|
|
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
|
-
|
|
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
|
}
|